diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java index 3180d57d2..d552647d2 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -11,6 +11,7 @@ import android.text.TextUtils; import android.util.Log; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable; import org.fdroid.fdroid.data.Schema.ApkTable; import org.fdroid.fdroid.data.Schema.AppMetadataTable; import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; @@ -133,6 +134,12 @@ public class AppProvider extends FDroidProvider { Uri uri = Uri.withAppendedPath(AppProvider.getContentUri(), PATH_CALC_PREFERRED_METADATA); context.getContentResolver().query(uri, null, null, null, null); } + + public static List findInstalledAppsWithKnownVulns(Context context) { + Uri uri = getInstalledWithKnownVulnsUri(); + Cursor cursor = context.getContentResolver().query(uri, Cols.ALL, null, null, null); + return cursorToList(cursor); + } } /** @@ -150,6 +157,7 @@ public class AppProvider extends FDroidProvider { protected static class AppQuerySelection extends QuerySelection { private boolean naturalJoinToInstalled; + private boolean naturalJoinAntiFeatures; private boolean leftJoinPrefs; AppQuerySelection() { @@ -170,6 +178,10 @@ public class AppProvider extends FDroidProvider { return naturalJoinToInstalled; } + public boolean naturalJoinAntiFeatures() { + return naturalJoinAntiFeatures; + } + /** * Tells the query selection that it will need to join onto the installed apps table * when used. This should be called when your query makes use of fields from that table @@ -182,6 +194,11 @@ public class AppProvider extends FDroidProvider { return this; } + public AppQuerySelection requireNatrualJoinAntiFeatures() { + naturalJoinAntiFeatures = true; + return this; + } + public boolean leftJoinToPrefs() { return leftJoinPrefs; } @@ -201,6 +218,11 @@ public class AppProvider extends FDroidProvider { if (this.leftJoinToPrefs() || query.leftJoinToPrefs()) { bothWithJoin.requireLeftJoinPrefs(); } + + if (this.naturalJoinAntiFeatures() || query.naturalJoinAntiFeatures()) { + bothWithJoin.requireNatrualJoinAntiFeatures(); + } + return bothWithJoin; } @@ -210,6 +232,7 @@ public class AppProvider extends FDroidProvider { private boolean isSuggestedApkTableAdded; private boolean requiresInstalledTable; + private boolean requiresAntiFeatures; private boolean requiresLeftJoinToPrefs; private boolean countFieldAppended; @@ -243,6 +266,9 @@ public class AppProvider extends FDroidProvider { if (selection.leftJoinToPrefs()) { leftJoinToPrefs(); } + if (selection.naturalJoinAntiFeatures()) { + naturalJoinAntiFeatures(); + } } // TODO: What if the selection requires a natural join, but we first get a left join @@ -277,6 +303,22 @@ public class AppProvider extends FDroidProvider { } } + public void naturalJoinAntiFeatures() { + if (!requiresAntiFeatures) { + join( + getApkAntiFeatureJoinTableName(), + "apkAntiFeature", + "apkAntiFeature." + ApkAntiFeatureJoinTable.Cols.APK_ID + " = " + getApkTableName() + "." + ApkTable.Cols.ROW_ID); + + join( + Schema.AntiFeatureTable.NAME, + "antiFeature", + "antiFeature." + Schema.AntiFeatureTable.Cols.ROW_ID + " = " + "apkAntiFeature." + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID); + + requiresAntiFeatures = true; + } + } + @Override public void addField(String field) { switch (field) { @@ -370,6 +412,7 @@ public class AppProvider extends FDroidProvider { private static final String PATH_CALC_PREFERRED_METADATA = "calcPreferredMetadata"; private static final String PATH_CALC_SUGGESTED_APKS = "calcNonRepoDetailsFromIndex"; private static final String PATH_TOP_FROM_CATEGORY = "topFromCategory"; + private static final String PATH_INSTALLED_WITH_KNOWN_VULNS = "installedWithKnownVulns"; private static final int CAN_UPDATE = CODE_SINGLE + 1; private static final int INSTALLED = CAN_UPDATE + 1; @@ -383,6 +426,7 @@ public class AppProvider extends FDroidProvider { private static final int HIGHEST_PRIORITY = SEARCH_REPO + 1; private static final int CALC_PREFERRED_METADATA = HIGHEST_PRIORITY + 1; private static final int TOP_FROM_CATEGORY = CALC_PREFERRED_METADATA + 1; + private static final int INSTALLED_WITH_KNOWN_VULNS = TOP_FROM_CATEGORY + 1; static { MATCHER.addURI(getAuthority(), null, CODE_LIST); @@ -400,6 +444,7 @@ public class AppProvider extends FDroidProvider { MATCHER.addURI(getAuthority(), PATH_SPECIFIC_APP + "/#/*", CODE_SINGLE); MATCHER.addURI(getAuthority(), PATH_CALC_PREFERRED_METADATA, CALC_PREFERRED_METADATA); MATCHER.addURI(getAuthority(), PATH_TOP_FROM_CATEGORY + "/#/*", TOP_FROM_CATEGORY); + MATCHER.addURI(getAuthority(), PATH_INSTALLED_WITH_KNOWN_VULNS, INSTALLED_WITH_KNOWN_VULNS); } public static Uri getContentUri() { @@ -421,6 +466,12 @@ public class AppProvider extends FDroidProvider { .build(); } + public static Uri getInstalledWithKnownVulnsUri() { + return getContentUri().buildUpon() + .appendPath(PATH_INSTALLED_WITH_KNOWN_VULNS) + .build(); + } + public static Uri getTopFromCategoryUri(String category, int limit) { return getContentUri().buildUpon() .appendPath(PATH_TOP_FROM_CATEGORY) @@ -505,6 +556,10 @@ public class AppProvider extends FDroidProvider { return ApkTable.NAME; } + protected String getApkAntiFeatureJoinTableName() { + return ApkAntiFeatureJoinTable.NAME; + } + @Override protected String getProviderName() { return "AppProvider"; @@ -652,6 +707,14 @@ public class AppProvider extends FDroidProvider { return new AppQuerySelection(selection, args); } + private AppQuerySelection queryInstalledWithKnownVulns() { + // Include the hash in this check because otherwise any app with any vulnerable version will + // get returned. + String selection = " antiFeature." + Schema.AntiFeatureTable.Cols.NAME + " = 'KnownVuln' AND " + + getApkTableName() + "." + ApkTable.Cols.HASH + " = installed." + InstalledAppTable.Cols.HASH; + return new AppQuerySelection(selection).requireNaturalInstalledTable().requireNatrualJoinAntiFeatures(); + } + static AppQuerySelection queryPackageNames(String packageNames, String packageNameField) { String[] args = packageNames.split(","); String selection = packageNameField + " IN (" + generateQuestionMarksForInClause(args.length) + ")"; @@ -739,6 +802,11 @@ public class AppProvider extends FDroidProvider { includeSwap = false; break; + case INSTALLED_WITH_KNOWN_VULNS: + selection = selection.add(queryInstalledWithKnownVulns()); + includeSwap = false; + break; + case RECENTLY_UPDATED: String table = getTableName(); String isNew = table + "." + Cols.LAST_UPDATED + " <= " + table + "." + Cols.ADDED + " DESC"; diff --git a/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java index 7c0843be1..064523b93 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java @@ -126,6 +126,10 @@ public class TempAppProvider extends AppProvider { return TempApkProvider.TABLE_TEMP_APK; } + protected String getApkAntiFeatureJoinTableName() { + return TempApkProvider.TABLE_TEMP_APK; + } + @Override public Uri insert(Uri uri, ContentValues values) { switch (MATCHER.match(uri)) { diff --git a/app/src/test/java/org/fdroid/fdroid/AntiFeaturesTest.java b/app/src/test/java/org/fdroid/fdroid/AntiFeaturesTest.java index a751c03c6..622a4ab7f 100644 --- a/app/src/test/java/org/fdroid/fdroid/AntiFeaturesTest.java +++ b/app/src/test/java/org/fdroid/fdroid/AntiFeaturesTest.java @@ -6,14 +6,17 @@ import android.content.ContentValues; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.FDroidProviderTest; +import org.fdroid.fdroid.data.InstalledAppTestUtils; import org.fdroid.fdroid.data.Schema; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import java.io.IOException; import java.util.List; import static org.junit.Assert.assertNull; @@ -24,26 +27,88 @@ import static org.junit.Assert.assertEquals; @RunWith(RobolectricTestRunner.class) public class AntiFeaturesTest extends FDroidProviderTest { - @Test - public void testPerApkAntiFeatures() throws IOException, RepoUpdater.UpdateException { + private App notVuln; + private App allVuln; + private App vulnAtV2; + + @Before + public void setup() { + Preferences.setup(context); + ContentValues vulnValues = new ContentValues(1); vulnValues.put(Schema.ApkTable.Cols.AntiFeatures.ANTI_FEATURES, "KnownVuln,ContainsGreenButtons"); - App vulnAtV2 = Assert.insertApp(context, "com.vuln", "Fixed it"); - Assert.insertApk(context, vulnAtV2, 1); - Assert.insertApk(context, vulnAtV2, 2, vulnValues); - Assert.insertApk(context, vulnAtV2, 3); + vulnAtV2 = Assert.insertApp(context, "com.vuln", "Fixed it"); + insertApk(vulnAtV2, 1, false); + insertApk(vulnAtV2, 2, true); + insertApk(vulnAtV2, 3, false); - App notVuln = Assert.insertApp(context, "com.not-vuln", "It's Fine"); - Assert.insertApk(context, notVuln, 5); - Assert.insertApk(context, notVuln, 10); - Assert.insertApk(context, notVuln, 15); + notVuln = Assert.insertApp(context, "com.not-vuln", "It's Fine"); + insertApk(notVuln, 5, false); + insertApk(notVuln, 10, false); + insertApk(notVuln, 15, false); - App allVuln = Assert.insertApp(context, "com.all-vuln", "Oops"); - Assert.insertApk(context, allVuln, 100, vulnValues); - Assert.insertApk(context, allVuln, 101, vulnValues); - Assert.insertApk(context, allVuln, 105, vulnValues); + allVuln = Assert.insertApp(context, "com.all-vuln", "Oops"); + insertApk(allVuln, 100, true); + insertApk(allVuln, 101, true); + insertApk(allVuln, 105, true); + AppProvider.Helper.recalculatePreferredMetadata(context); + } + + @After + public void tearDown() { + Preferences.clearSingletonForTesting(); + } + + private static String generateHash(String packageName, int versionCode) { + return packageName + "-" + versionCode; + } + + private void insertApk(App app, int versionCode, boolean isVuln) { + ContentValues values = new ContentValues(); + values.put(Schema.ApkTable.Cols.HASH, generateHash(app.packageName, versionCode)); + if (isVuln) { + values.put(Schema.ApkTable.Cols.AntiFeatures.ANTI_FEATURES, "KnownVuln,ContainsGreenButtons"); + } + Assert.insertApk(context, app, versionCode, values); + } + + private void install(App app, int versionCode) { + String hash = generateHash(app.packageName, versionCode); + InstalledAppTestUtils.install(context, app.packageName, versionCode, "v" + versionCode, null, hash); + } + + @Test + public void noVulnerableApps() { + List installed = AppProvider.Helper.findInstalledAppsWithKnownVulns(context); + assertEquals(0, installed.size()); + } + + @Test + public void futureVersionIsVulnerable() { + install(vulnAtV2, 1); + List installed = AppProvider.Helper.findInstalledAppsWithKnownVulns(context); + assertEquals(0, installed.size()); + } + + @Test + public void vulnerableAndAbleToBeUpdated() { + install(vulnAtV2, 2); + List installed = AppProvider.Helper.findInstalledAppsWithKnownVulns(context); + assertEquals(1, installed.size()); + assertEquals(vulnAtV2.packageName, installed.get(0).packageName); + } + + @Test + public void vulnerableButUpToDate() { + install(vulnAtV2, 3); + List installed = AppProvider.Helper.findInstalledAppsWithKnownVulns(context); + assertEquals(0, installed.size()); + } + + @Test + public void antiFeaturesSaveCorrectly() { List notVulnApks = ApkProvider.Helper.findByPackageName(context, notVuln.packageName); assertEquals(3, notVulnApks.size()); diff --git a/app/src/test/java/org/fdroid/fdroid/data/InstalledAppTestUtils.java b/app/src/test/java/org/fdroid/fdroid/data/InstalledAppTestUtils.java index f4b599ae4..5926899de 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/InstalledAppTestUtils.java +++ b/app/src/test/java/org/fdroid/fdroid/data/InstalledAppTestUtils.java @@ -22,6 +22,14 @@ public class InstalledAppTestUtils { String packageName, int versionCode, String versionName, @Nullable String signingCert) { + install(context, packageName, versionCode, versionName, signingCert, null); + } + + public static void install(Context context, + String packageName, + int versionCode, String versionName, + @Nullable String signingCert, + @Nullable String hash) { PackageInfo info = new PackageInfo(); info.packageName = packageName; info.versionCode = versionCode; @@ -31,8 +39,12 @@ public class InstalledAppTestUtils { if (signingCert != null) { info.signatures = new Signature[]{new Signature(signingCert)}; } + String hashType = "sha256"; - String hash = "00112233445566778899aabbccddeeff"; + if (hash == null) { + hash = "00112233445566778899aabbccddeeff"; + } + InstalledAppProviderService.insertAppIntoDb(context, info, hashType, hash); }