From a0f6a002441e1515413830c23929c844279eab77 Mon Sep 17 00:00:00 2001 From: Tad Date: Thu, 28 Dec 2023 22:18:35 -0500 Subject: [PATCH] Many changes - Fix signature count reseting on update but not changed - Better handle reloads for extended - Metered connection warning - Block all actions when appropriate - Use a wakelock instead of keeping the screen on - Increase max scan sizes - Always check if files can be read before scanning - Resolve true paths to avoid duplicates from symlinks - Scan more app paths - Scan more system paths - Bump version Signed-off-by: Tad --- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 1 + .../us/spotco/malwarescanner/Database.java | 10 +- .../spotco/malwarescanner/MainActivity.java | 221 ++++++++++++------ .../spotco/malwarescanner/MalwareScanner.java | 1 + .../malwarescanner/MalwareScannerService.java | 2 +- .../java/us/spotco/malwarescanner/Utils.java | 21 +- app/src/main/res/values/strings.xml | 7 +- .../metadata/android/en-US/changelogs/305.txt | 10 + 9 files changed, 190 insertions(+), 89 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/305.txt diff --git a/app/build.gradle b/app/build.gradle index f3d3ff1..328180a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "us.spotco.malwarescanner" minSdkVersion 16 targetSdkVersion 32 - versionCode 302 - versionName "3.02" + versionCode 305 + versionName "3.05" resConfigs 'en', 'af', 'de', 'el', 'es', 'fi', 'fr', 'it', 'pl', 'pt', 'ru', 'tr', 'zh-rCN' } buildTypes { @@ -20,7 +20,7 @@ android { shrinkResources true minifyEnabled true zipAlignEnabled true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } lint { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2decd6b..d04faee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> + diff --git a/app/src/main/java/us/spotco/malwarescanner/Database.java b/app/src/main/java/us/spotco/malwarescanner/Database.java index 5cdb246..3dca50b 100644 --- a/app/src/main/java/us/spotco/malwarescanner/Database.java +++ b/app/src/main/java/us/spotco/malwarescanner/Database.java @@ -52,7 +52,8 @@ class Database { public static BloomFilter signaturesSHA1 = null; public static BloomFilter signaturesSHA256 = null; public static long signaturesCount = 0; - public static boolean changed = false; + public static boolean changedDownload = false; + public static boolean changedConfig = false; private static final DateFormat dateFormat = DateFormat.getDateInstance(); @@ -87,7 +88,7 @@ class Database { if (!Utils.getDatabaseURL(context).equals(Utils.DATABASE_URL_DEFAULT)) { log.append(context.getString(R.string.main_database_override, Utils.getDatabaseURL(context)) + "\n"); } - changed = false; + changedDownload = false; boolean onionRouting = prefs.getBoolean("ONION_ROUTING", false); new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, Utils.getDatabaseURL(context) + "gpg.key", databasePath + "/gpg.key", Utils.getDatabaseURL(context)); @@ -102,7 +103,6 @@ class Database { databasePath.mkdir(); signatureDatabases.clear(); - signaturesCount = 0; prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE); String baseURL = Utils.getDatabaseURL(context); signatureDatabases.add(new SignatureDatabase(baseURL, "hypatia-md5-bloom.bin")); @@ -119,6 +119,8 @@ class Database { databaseCurrentlyLoading = true; initDatabase(context); signaturesCount = 0; + changedConfig = false; + signaturesMD5Extended = null; File publicKey = new File(databasePath + "/gpg.key"); GPGDetachedSignatureVerifier verifier = new GPGDetachedSignatureVerifier(Utils.getSigningKey(context)); for (SignatureDatabase database : signatureDatabases) { @@ -227,7 +229,7 @@ class Database { fileOutputStream.close(); outNew.renameTo(out); //Move the new file into place - changed = true; + changedDownload = true; publishProgress(url.replaceAll(baseURL, "") + "\n\t" + Utils.getContext().getString(R.string.main_database_download_success) diff --git a/app/src/main/java/us/spotco/malwarescanner/MainActivity.java b/app/src/main/java/us/spotco/malwarescanner/MainActivity.java index c07de3c..19f58af 100644 --- a/app/src/main/java/us/spotco/malwarescanner/MainActivity.java +++ b/app/src/main/java/us/spotco/malwarescanner/MainActivity.java @@ -32,6 +32,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; +import android.os.PowerManager; import android.provider.Settings; import android.text.InputType; import android.text.method.ScrollingMovementMethod; @@ -75,8 +76,6 @@ public class MainActivity extends Activity { Utils.setContext(getApplicationContext()); setContentView(R.layout.content_main); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - logView = findViewById(R.id.txtLogOutput); logView.setMovementMethod(new ScrollingMovementMethod()); logView.setTextIsSelectable(true); @@ -154,62 +153,101 @@ public class MainActivity extends Activity { } break; case R.id.mnuUpdateDatabase: - if (malwareScanner.running) { + if (Database.hasDownloadsRunning()) { + logView.append(getString(R.string.lblUpdateRunning) + "\n"); + } else if (malwareScanner.running) { logView.append(getString(R.string.lblScanRunning) + "\n"); } else if (!Utils.isNetworkAvailable(this)) { logView.append(getString(R.string.lblNoNetwork) + "\n"); + } else if (Database.isDatabaseLoading()) { + logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + } else if (Utils.isConnectionMetered(this)) { + int amt = prefs.getBoolean("SIGNATURES_EXTENDED", false) ? 200 : 50; + new AlertDialog.Builder(this) + .setTitle(R.string.confirm_update_title) + .setMessage(getString(R.string.confirm_update_summary, String.valueOf(amt))) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(getString(android.R.string.yes), (dialog, which) -> { + updateDatabase(); + }) + .setNegativeButton(getString(android.R.string.no), (dialog, which) -> { + dialog.cancel(); + }).show(); } else { - if (prefs.getBoolean("ONION_ROUTING", false)) { - Utils.requestStartOrbot(this); - logView.append(getString(R.string.lblOnionRoutingEnabledHint) + "\n"); - } updateDatabase(); } break; case R.id.mnuDatabaseServer: - AlertDialog.Builder builderServerOverride = new AlertDialog.Builder(this); - builderServerOverride.setTitle(getString(R.string.lblDatabaseServer)); - final EditText inputServerOverride = new EditText(this); - inputServerOverride.setInputType(InputType.TYPE_CLASS_TEXT); - inputServerOverride.setText(Utils.getDatabaseURL(this)); - builderServerOverride.setView(inputServerOverride); - builderServerOverride.setPositiveButton(getString(R.string.lblOverride), (dialog, which) -> { - String newServer = inputServerOverride.getText().toString(); - if (!newServer.endsWith("/")) { - newServer += "/"; - } - prefs.edit().putString("DATABASE_SERVER", newServer).apply(); - }); - builderServerOverride.setNegativeButton(getString(R.string.lblReset), (dialog, which) -> { - prefs.edit().putString("DATABASE_SERVER", Utils.DATABASE_URL_DEFAULT).apply(); - dialog.cancel(); - }); - builderServerOverride.show(); + if (Database.hasDownloadsRunning()) { + logView.append(getString(R.string.lblUpdateRunning) + "\n"); + } else if (Database.isDatabaseLoading()) { + logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + } else { + AlertDialog.Builder builderServerOverride = new AlertDialog.Builder(this); + builderServerOverride.setTitle(getString(R.string.lblDatabaseServer)); + final EditText inputServerOverride = new EditText(this); + inputServerOverride.setInputType(InputType.TYPE_CLASS_TEXT); + inputServerOverride.setText(Utils.getDatabaseURL(this)); + builderServerOverride.setView(inputServerOverride); + builderServerOverride.setPositiveButton(getString(R.string.lblOverride), (dialog, which) -> { + String newServer = inputServerOverride.getText().toString(); + if (!newServer.endsWith("/")) { + newServer += "/"; + } + prefs.edit().putString("DATABASE_SERVER", newServer).apply(); + }); + builderServerOverride.setNegativeButton(getString(R.string.lblReset), (dialog, which) -> { + prefs.edit().putString("DATABASE_SERVER", Utils.DATABASE_URL_DEFAULT).apply(); + dialog.cancel(); + }); + builderServerOverride.show(); + } break; case R.id.mnuSigningKey: - AlertDialog.Builder builderKey = new AlertDialog.Builder(this); - builderKey.setTitle(getString(R.string.lblSigningKey)); - final EditText inputKey = new EditText(this); - inputKey.setInputType(InputType.TYPE_CLASS_TEXT); - inputKey.setText(Utils.getSigningKey(this)); - builderKey.setView(inputKey); - builderKey.setPositiveButton(getString(R.string.lblOverride), (dialog, which) -> prefs.edit().putString("SIGNING_KEY", inputKey.getText().toString()).apply()); - builderKey.setNegativeButton(getString(R.string.lblReset), (dialog, which) -> { - prefs.edit().putString("SIGNING_KEY", Utils.SIGNING_KEY_DEFAULT).apply(); - dialog.cancel(); - }); - builderKey.show(); + if (Database.hasDownloadsRunning()) { + logView.append(getString(R.string.lblUpdateRunning) + "\n"); + } else if (Database.isDatabaseLoading()) { + logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + } else { + AlertDialog.Builder builderKey = new AlertDialog.Builder(this); + builderKey.setTitle(getString(R.string.lblSigningKey)); + final EditText inputKey = new EditText(this); + inputKey.setInputType(InputType.TYPE_CLASS_TEXT); + inputKey.setText(Utils.getSigningKey(this)); + builderKey.setView(inputKey); + builderKey.setPositiveButton(getString(R.string.lblOverride), (dialog, which) -> prefs.edit().putString("SIGNING_KEY", inputKey.getText().toString()).apply()); + builderKey.setNegativeButton(getString(R.string.lblReset), (dialog, which) -> { + prefs.edit().putString("SIGNING_KEY", Utils.SIGNING_KEY_DEFAULT).apply(); + dialog.cancel(); + }); + builderKey.show(); + } break; case R.id.toggleExtended: - new AlertDialog.Builder(this) - .setTitle(R.string.confirm_extended_title) - .setMessage(getString(R.string.confirm_extended_summary)) - .setIcon(android.R.drawable.ic_menu_compass) - .setPositiveButton(getString(android.R.string.yes), (dialog, which) -> prefs.edit().putBoolean("SIGNATURES_EXTENDED", true).apply()) - .setNegativeButton(getString(android.R.string.no), (dialog, which) -> { - prefs.edit().putBoolean("SIGNATURES_EXTENDED", false).apply(); - dialog.cancel(); - }).show(); + if (Database.hasDownloadsRunning()) { + logView.append(getString(R.string.lblUpdateRunning) + "\n"); + } else if (Database.isDatabaseLoading()) { + logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + } else { + boolean prevExtended = prefs.getBoolean("SIGNATURES_EXTENDED", false); + new AlertDialog.Builder(this) + .setTitle(R.string.confirm_extended_title) + .setMessage(getString(R.string.confirm_extended_summary)) + .setIcon(android.R.drawable.ic_menu_compass) + .setPositiveButton(getString(android.R.string.yes), (dialog, which) -> { + prefs.edit().putBoolean("SIGNATURES_EXTENDED", true).apply(); + if (!prevExtended) { + Database.changedConfig = true; + } + }) + .setNegativeButton(getString(android.R.string.no), (dialog, which) -> { + prefs.edit().putBoolean("SIGNATURES_EXTENDED", false).apply(); + if (prevExtended) { + Database.changedConfig = true; + } + dialog.cancel(); + }).show(); + } break; case R.id.toggleRealtime: if (malwareScanner.running) { @@ -250,8 +288,14 @@ public class MainActivity extends Activity { break; case R.id.btnStartScan: if (!malwareScanner.running) { - updateScanButton(true); - startScanner(); + if (Database.hasDownloadsRunning()) { + logView.append(getString(R.string.lblUpdateRunning) + "\n"); + } else if (Database.isDatabaseLoading()) { + logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + } else { + updateScanButton(true); + startScanner(); + } } else { logView.append("\n" + getString(R.string.main_cancelling_scan) + "\n\n"); malwareScanner.cancel(true); @@ -267,20 +311,41 @@ public class MainActivity extends Activity { HashSet filesToScan = new HashSet<>(); if (scanSystem) { filesToScan.add(Environment.getRootDirectory()); + filesToScan.add(new File("/")); + filesToScan.add(new File("/apex")); + filesToScan.add(new File("/cache")); + filesToScan.add(new File("/data")); + filesToScan.add(new File("/data/local/tmp")); + filesToScan.add(new File("/firmware")); + filesToScan.add(new File("/odm")); + filesToScan.add(new File("/odm_dlkm")); + filesToScan.add(new File("/product")); + filesToScan.add(new File("/system")); + filesToScan.add(new File("/system_dlkm")); + filesToScan.add(new File("/vendor")); + filesToScan.add(new File("/vendor_dlkm")); } if (scanApps) { for (ApplicationInfo packageInfo : getPackageManager().getInstalledApplications(PackageManager.GET_META_DATA)) { filesToScan.add(new File(packageInfo.sourceDir)); - //Log.d("Hypatia", "Planning to scan " + packageInfo.sourceDir); + filesToScan.add(new File(packageInfo.dataDir)); + filesToScan.add(new File(packageInfo.nativeLibraryDir)); + filesToScan.add(new File(packageInfo.publicSourceDir)); } } if (scanInternal) { filesToScan.add(Environment.getExternalStorageDirectory()); } if (scanExternal) { - filesToScan.add(new File("/storage")); + File externalStorage = new File("/storage"); + if (externalStorage.exists()) { + filesToScan.add(externalStorage); + } } + PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); + PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Hypatia::ManualScanLock"); + wakeLock.acquire(10 * 60 * 1000L); /* 10 minutes */ malwareScanner.executeOnExecutor(Utils.getThreadPoolExecutor(), filesToScan); new Thread(() -> { try { @@ -288,6 +353,7 @@ public class MainActivity extends Activity { Thread.sleep(500); } runOnUiThread(() -> updateScanButton(false)); + wakeLock.release(); } catch (Exception e) { e.printStackTrace(); } @@ -295,36 +361,43 @@ public class MainActivity extends Activity { } private void updateDatabase() { + if (prefs.getBoolean("ONION_ROUTING", false)) { + Utils.requestStartOrbot(this); + logView.append(getString(R.string.lblOnionRoutingEnabledHint) + "\n"); + } new Database(findViewById(R.id.txtLogOutput)); if (!Database.isDatabaseLoading()) { + PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); + PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Hypatia::UpdateLock"); + wakeLock.acquire(3 * 60 * 1000L); /* 3 minutes */ Database.updateDatabase(this, Database.signatureDatabases); + Utils.getThreadPoolExecutor().execute(() -> { + try { + Thread.sleep(500); + Log.d("Hypatia", "Considering database reload!"); + while (Database.hasDownloadsRunning()) { + Thread.sleep(500); + Log.d("Hypatia", "Download in progress, waiting!"); + } + wakeLock.release(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + runOnUiThread(() -> logView.append(getString(R.string.lblDatabasesUpdated) + "\n")); + if (Database.isDatabaseLoaded()) { + if (Database.changedDownload || Database.changedConfig) { + Log.d("Hypatia", "Really reloading database!"); + Database.loadDatabase(getApplicationContext(), true, Database.signatureDatabases); + } else { + Log.d("Hypatia", "Database not changed, skipping reload!"); + } + } else { + Log.d("Hypatia", "Database not loaded, skipping reload!"); + } + }); } else { logView.append(getString(R.string.lblDatabaseLoading) + "\n"); } - Utils.getThreadPoolExecutor().execute(() -> { - try { - Thread.sleep(500); - Log.d("Hypatia", "Considering database reload!"); - while (Database.hasDownloadsRunning()) { - Thread.sleep(500); - Log.d("Hypatia", "Download in progress, waiting!"); - - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - runOnUiThread(() -> logView.append(getString(R.string.lblDatabasesUpdated) + "\n")); - if (Database.isDatabaseLoaded()) { - if(Database.changed) { - Log.d("Hypatia", "Really reloading database!"); - Database.loadDatabase(getApplicationContext(), true, Database.signatureDatabases); - } else { - Log.d("Hypatia", "Database not changed, skipping reload!"); - } - } else { - Log.d("Hypatia", "Database not loaded, skipping reload!"); - } - }); } private void updateScanButton(boolean running) { diff --git a/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java b/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java index 46401cb..c5c80d3 100644 --- a/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java +++ b/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java @@ -221,6 +221,7 @@ class MalwareScanner extends AsyncTask, Object, String> { spinnerCur = " = "; } } + //Log.d("Hypatia", "Scanning " + file); } filesToScanReal.clear(); publishProgress("\n\t" + context.getString(R.string.main_hashing_done) + "\n", true); diff --git a/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java b/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java index ec39b64..c4127ed 100644 --- a/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java +++ b/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java @@ -79,7 +79,7 @@ public class MalwareScannerService extends Service { case FileObserver.MOVED_TO: case FileObserver.CLOSE_WRITE: File file = new File(path); - if (file.exists() && /*file.length() > 0 &&*/ file.length() <= Utils.MAX_SCAN_SIZE_REALTIME) { + if (file.exists() && file.length() <= Utils.MAX_SCAN_SIZE_REALTIME) { HashSet filesToScan = new HashSet<>(); filesToScan.add(file); new MalwareScanner(null, getApplicationContext(), false).executeOnExecutor(threadPoolExecutor, filesToScan); diff --git a/app/src/main/java/us/spotco/malwarescanner/Utils.java b/app/src/main/java/us/spotco/malwarescanner/Utils.java index 4828844..4a15dfd 100644 --- a/app/src/main/java/us/spotco/malwarescanner/Utils.java +++ b/app/src/main/java/us/spotco/malwarescanner/Utils.java @@ -41,8 +41,8 @@ import java.util.concurrent.atomic.AtomicInteger; class Utils { private static Context context = null; - public final static int MAX_SCAN_SIZE = (1000 * 1000) * 80; //80MB - public final static int MAX_SCAN_SIZE_REALTIME = MAX_SCAN_SIZE / 2; //40MB + public final static int MAX_SCAN_SIZE = (1000 * 1000) * 500; //500MB + public final static int MAX_SCAN_SIZE_REALTIME = (1000 * 1000) * 250; //250MB public final static String DATABASE_URL_DEFAULT = "https://divested.dev/MalwareScannerSignatures/"; public final static String SIGNING_KEY_DEFAULT = "BADFCABDDBF5B694"; @@ -75,8 +75,14 @@ class Utils { public static HashSet getFilesRecursive(File root) { HashSet filesAll = new HashSet<>(); - if (root.isFile()) { //TODO: Skip this - filesAll.add(root); + try { + root = root.getCanonicalFile(); + } catch (Exception ignored) { + } + if (root.isFile()) { + if (root.canRead() && root.length() <= MAX_SCAN_SIZE) { + filesAll.add(root); + } return filesAll; } else { File[] files = root.listFiles(); @@ -85,7 +91,7 @@ class Utils { if (f.isDirectory()) { filesAll.addAll(getFilesRecursive(f)); } else { - if (f.length() <= MAX_SCAN_SIZE && f.canRead()) {//Exclude files larger than limit for performance + if (f.isFile() && f.canRead() && f.length() <= MAX_SCAN_SIZE) {//Exclude files larger than limit for performance filesAll.add(f); } } @@ -177,6 +183,11 @@ class Utils { } } + public static boolean isConnectionMetered(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + return connectivityManager != null && connectivityManager.isActiveNetworkMetered(); + } + //Credit: https://stackoverflow.com/a/4239019 public static boolean isNetworkAvailable(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fe81e8d..a237d63 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,7 +53,7 @@ No network connected! Self test successful. Self test failed! - Database is loading, not updating! + Skipping action, database is loading! All databases updated! Lookup Delete @@ -71,5 +71,8 @@ Write self test files Extended datatabase Enable extended database? - This will enable detection of an additional ~40 million signatures.\nThis requires a 125MB download, will slow down startup by over two minutes, will increase app RAM usage, and will increase the false positive rate.\nThis database only updates quarterly. + [EXPERIMENTAL]\nThis will enable detection of an additional ~40 million signatures.\nThis requires a 125MB download, will slow down startup by over two minutes, will increase app RAM usage, and will increase the false positive rate.\nThis database only updates quarterly. + Confirm download + You appear to be on a metered connection. Are you sure you want to update the databases?\nIt may download up to %s megabytes of data. + Skipping action, an update is running! diff --git a/fastlane/metadata/android/en-US/changelogs/305.txt b/fastlane/metadata/android/en-US/changelogs/305.txt new file mode 100644 index 0000000..64942cd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/305.txt @@ -0,0 +1,10 @@ +* Fix signature count reseting on update but not changed +* Better handle reloads when extended DB is enabled +* Metered connection warning +* Block all actions when appropriate +* Use a wakelock instead of keeping the screen on +* Increase max scan sizes: 500MB manual, 250MB realtime +* Always check if files can be read before scanning +* Resolve true paths to avoid duplicates from symlinks +* Scan more app paths +* Scan more system paths