From eccaaadd5df7103c16e815496514bf83b6e917a2 Mon Sep 17 00:00:00 2001 From: Tad Date: Mon, 22 Feb 2021 11:04:05 -0500 Subject: [PATCH] Many small improvements - Fix an exception with the notification counter - Ensure the database is really loaded before a scan - Inform the user when a scan is cancelled - Allow quicker cancelling of scan (when in the middle of hashing) - Don't block main thread when updating databases - Change the fab color depending on the scan state - Add a simple textual progress bar to scans - Make text output selectable - Add a test signature - Small sanity checks Partially fixes https://github.com/Divested-Mobile/hypatia/issues/1#issuecomment-707734221 Closes https://gitlab.com/divested-mobile/hypatia/-/issues/3 --- .idea/caches/build_file_checksums.ser | Bin 532 -> 532 bytes app/build.gradle | 4 +- .../us/spotco/malwarescanner/Database.java | 37 ++++++------ .../spotco/malwarescanner/MainActivity.java | 53 ++++++++++++---- .../spotco/malwarescanner/MalwareScanner.java | 57 +++++++++++++++--- .../malwarescanner/MalwareScannerService.java | 6 +- .../java/us/spotco/malwarescanner/Utils.java | 5 +- app/src/main/res/values-de/strings.xml | 2 - app/src/main/res/values-fr/strings.xml | 2 - app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 7 ++- graphics/hypatia-test-file.png | Bin 0 -> 13307 bytes graphics/hypatia-test-file.xcf | Bin 0 -> 42124 bytes 13 files changed, 122 insertions(+), 52 deletions(-) create mode 100644 graphics/hypatia-test-file.png create mode 100644 graphics/hypatia-test-file.xcf diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index b96a427df6638c244ba31d2121d0c7f0a7d3cd32..2324abe40eb012779522c2db1d66b9bcea725cbe 100644 GIT binary patch delta 33 rcmV++0N($U1e64jm;}oUn*Xt!X8{qP==1IFo signatureDatabases = new HashSet<>(); public final static String baseURL = "https://divested.dev/MalwareScannerSignatures/"; @@ -56,7 +56,7 @@ class Database { public final static HashMap signaturesSHA1 = new HashMap<>(); public final static HashMap signaturesSHA256 = new HashMap<>(); - private static final DateFormat dateFormat = new SimpleDateFormat(Utils.getContext().getString(R.string.simple_date_format_short)); + private static final DateFormat dateFormat = DateFormat.getDateInstance(); public Database(TextView log) { Database.log = log; @@ -68,7 +68,7 @@ class Database { } public static boolean isDatabaseLoaded() { - return signaturesMD5.size() > 0 && signaturesSHA1.size() > 0 && signaturesSHA256.size() > 0; + return databaseFullyLoaded && signaturesMD5.size() > 0 && signaturesSHA1.size() > 0 && signaturesSHA256.size() > 0; } public static int getSignatureCount() { @@ -77,7 +77,7 @@ class Database { public static void updateDatabase(Context context, HashSet signatureDatabases) { initDatabase(context); - log.append(context.getString(R.string.main_database_updating, signatureDatabases.size() + "")+ "\n"); + log.append(context.getString(R.string.main_database_updating, signatureDatabases.size() + "") + "\n"); for (SignatureDatabase signatureDatabase : signatureDatabases) { boolean onionRouting = prefs.getBoolean("ONION_ROUTING", false); new Downloader().executeOnExecutor(threadPoolExecutor, onionRouting, signatureDatabase.getUrl(), databasePath + "/" + signatureDatabase.getName()); @@ -114,6 +114,7 @@ class Database { public static void loadDatabase(Context context, boolean ignoreifLoaded, HashSet signatureDatabases) { if (!isDatabaseLoaded() || !ignoreifLoaded && isDatabaseLoaded()) { + databaseFullyLoaded = false; initDatabase(context); signaturesMD5.clear(); signaturesSHA1.clear(); @@ -132,22 +133,22 @@ class Database { String line; if (database.getName().contains(".hdb")) {//.hdb format: md5, size, name while ((line = reader.readLine()) != null) { - String[] lineS = line.split(":"); - if (Utils.TRIM_VARIANT_NUMBER) { - lineS[2] = lineS[2].split("-")[0]; + if (line.length() > 0) { + String[] lineS = line.split(":"); + if (lineS[0].length() > 0) { + signaturesMD5.put(lineS[0].substring(0, Utils.MAX_HASH_LENGTH), lineS[2]); + } } - signaturesMD5.put(lineS[0].substring(0, Utils.MAX_HASH_LENGTH), lineS[2]); } } else if (database.getName().contains(".hsb")) {//.hsb format: sha256, size, name while ((line = reader.readLine()) != null) { - String[] lineS = line.split(":"); - if (Utils.TRIM_VARIANT_NUMBER) { - lineS[2] = lineS[2].split("-")[0]; - } - if (lineS[0].length() == 32) { - signaturesSHA1.put(lineS[0].substring(0, Utils.MAX_HASH_LENGTH), lineS[2]); - } else { - signaturesSHA256.put(lineS[0].substring(0, Utils.MAX_HASH_LENGTH), lineS[2]); + if (line.length() > 0) { + String[] lineS = line.split(":"); + if (lineS[0].length() == 32) { + signaturesSHA1.put(lineS[0].substring(0, Utils.MAX_HASH_LENGTH), lineS[2]); + } else if (lineS[0].length() > 0) { + signaturesSHA256.put(lineS[0].substring(0, Utils.MAX_HASH_LENGTH), lineS[2]); + } } } } @@ -158,7 +159,9 @@ class Database { } } signaturesMD5.put("44d88612fea8a8f36de82e1278abb02f".substring(0, Utils.MAX_HASH_LENGTH), "Eicar-Test-Signature"); + signaturesSHA256.put("6a0b4866f143c32e651662cebf7f380d27b0db809db3b6a34cf34c7436ab6bbf".substring(0, Utils.MAX_HASH_LENGTH), "Hypatia-Test-Signature"); System.gc(); + databaseFullyLoaded = true; } } @@ -196,7 +199,7 @@ class Database { } FileOutputStream fileOutputStream = new FileOutputStream(out); - final byte data[] = new byte[1024]; + final byte[] data = new byte[1024]; int count; while ((count = connection.getInputStream().read(data, 0, 1024)) != -1) { fileOutputStream.write(data, 0, count); diff --git a/app/src/main/java/us/spotco/malwarescanner/MainActivity.java b/app/src/main/java/us/spotco/malwarescanner/MainActivity.java index bb85dcb..064fc3c 100644 --- a/app/src/main/java/us/spotco/malwarescanner/MainActivity.java +++ b/app/src/main/java/us/spotco/malwarescanner/MainActivity.java @@ -26,16 +26,10 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.os.AsyncTask; +import android.content.res.ColorStateList; +import android.os.Build; import android.os.Bundle; import android.os.Environment; -import com.google.android.material.floatingactionbutton.FloatingActionButton; - -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import android.text.method.ScrollingMovementMethod; import android.view.Menu; import android.view.MenuItem; @@ -44,6 +38,14 @@ import android.view.WindowManager; import android.widget.TextView; import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + import java.io.File; import java.util.HashSet; import java.util.Set; @@ -75,6 +77,7 @@ public class MainActivity extends AppCompatActivity { logView = findViewById(R.id.txtLogOutput); logView.setMovementMethod(new ScrollingMovementMethod()); + logView.setTextIsSelectable(true); logView.append(getString(R.string.app_copyright) + "\n"); logView.append(getString(R.string.app_license) + "\n"); logView.append(getString(R.string.app_db_type_clamav) + "\n\n"); @@ -83,15 +86,35 @@ public class MainActivity extends AppCompatActivity { prefs = getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE); - FloatingActionButton fab = findViewById(R.id.fab); + final FloatingActionButton fab = findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - if (!malwareScanner.getStatus().equals(AsyncTask.Status.RUNNING)) { + if (!malwareScanner.running) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + fab.setBackgroundTintList(ColorStateList.valueOf(getColor(R.color.red))); + } startScanner(); + Utils.getThreadPoolExecutor().execute(new Runnable() { + @Override + public void run() { + while (malwareScanner.running) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + fab.setBackgroundTintList(ColorStateList.valueOf(getColor(R.color.light_blue))); + } + + } + }); } else { + logView.append(getString(R.string.main_cancelling_scan) + "\n\n"); malwareScanner.cancel(true); - logView.append(getString(R.string.app_db_type_clamav) + "\n\n"); + malwareScanner.running = false; } } }); @@ -226,13 +249,19 @@ public class MainActivity extends AppCompatActivity { filesToScan.addAll(Utils.getFilesRecursive(new File("/storage"))); } malwareScanner.executeOnExecutor(Utils.getThreadPoolExecutor(), filesToScan); + malwareScanner.running = true; } private void updateDatabase() { new Database((TextView) findViewById(R.id.txtLogOutput)); Database.updateDatabase(this, Database.signatureDatabases); if (Database.isDatabaseLoaded()) { - Database.loadDatabase(this, false, Database.signatureDatabases); + Utils.getThreadPoolExecutor().execute(new Runnable() { + @Override + public void run() { + Database.loadDatabase(getApplicationContext(), true, Database.signatureDatabases); + } + }); } } diff --git a/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java b/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java index 2c2a499..7771f28 100644 --- a/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java +++ b/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java @@ -26,10 +26,10 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Environment; import android.os.SystemClock; -import androidx.core.app.NotificationCompat; -import android.util.Log; import android.widget.TextView; +import androidx.core.app.NotificationCompat; + import java.io.File; import java.io.FileInputStream; import java.io.InputStream; @@ -50,6 +50,7 @@ class MalwareScanner extends AsyncTask, Object, String> { private final HashMap fileHashesMD5 = new HashMap<>(); private final HashMap fileHashesSHA1 = new HashMap<>(); private final HashMap fileHashesSHA256 = new HashMap<>(); + public boolean running = false; public MalwareScanner(Activity activity, Context context, boolean userFacing) { this.context = context; @@ -68,7 +69,10 @@ class MalwareScanner extends AsyncTask, Object, String> { private void logResult(String result, boolean userFacingOnly) { if (userFacing) { - logOutput.append(result + "\n"); + logOutput.append(result); + if (!(result.length() <= 3)) { + logOutput.append("\n"); + } } else if (!userFacingOnly) { String[] malwareDetect = result.split(" in "); NotificationCompat.Builder mBuilder = @@ -79,13 +83,12 @@ class MalwareScanner extends AsyncTask, Object, String> { .setPriority(Notification.PRIORITY_MAX) .setDefaults(Notification.DEFAULT_VIBRATE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mBuilder.setVisibility(Notification.VISIBILITY_SECRET); + mBuilder.setVisibility(NotificationCompat.VISIBILITY_SECRET); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mBuilder.setChannelId("DETECTION"); } notificationManager.notify(new Random().nextInt(), mBuilder.build()); - //Log.d("Hypatia", result); } } @@ -97,6 +100,8 @@ class MalwareScanner extends AsyncTask, Object, String> { @Override protected final String doInBackground(Set[] filesToScan) { + running = true; + //Pre fileHashesMD5.clear(); fileHashesSHA1.clear(); @@ -105,15 +110,46 @@ class MalwareScanner extends AsyncTask, Object, String> { publishProgress("\t" + context.getString(R.string.main_files_pending_scan, filesToScan[0].size() + "") + "\n", true); Database.loadDatabase(context, true, Database.signatureDatabases); - if (Database.getSignatureCount() >= 0) { + int delayCount = 0; + while (!Database.isDatabaseLoaded() && delayCount <= 90) { + try { + Thread.sleep(1000); + delayCount++; + //publishProgress("\t" + context.getString(R.string.main_database_loading), true); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + if (Database.isDatabaseLoaded()) { publishProgress("\t" + context.getString(R.string.main_database_loaded, Database.getSignatureCount() + "") + "\n", true); //Get file hashes publishProgress("\t" + context.getString(R.string.main_hashing_files), true); - for (File file : filesToScan[0]) { - getFileHashes(file); + publishProgress("\t", true); + int fileScannedCount = 0; + int percentIncrement = (filesToScan[0].size() / 20); + if (percentIncrement < 1) { //Prevent divide by zero + percentIncrement = 1; } - publishProgress("\t" + context.getString(R.string.main_hashing_done) + "\n", true); + String spinnerCur = " ~ "; + for (File file : filesToScan[0]) { + if (this.isCancelled()) { //Allow quicker cancels + //publishProgress("\t" + context.getString(R.string.main_cancelled_scan), true); + running = false; + return null; + } + getFileHashes(file); + fileScannedCount++; + if ((fileScannedCount % percentIncrement) == 0) { + publishProgress(spinnerCur, true); + if (spinnerCur.equals(" = ")) { + spinnerCur = " ~ "; + } else { + spinnerCur = " = "; + } + } + } + publishProgress("\n\t" + context.getString(R.string.main_hashing_done) + "\n", true); //Check the hashes checkSignature("MD5", fileHashesMD5, Database.signaturesMD5); @@ -129,7 +165,9 @@ class MalwareScanner extends AsyncTask, Object, String> { publishProgress(context.getString(R.string.main_scanning_done, ((SystemClock.elapsedRealtime() - scanTime) / 1000) + "") + "\n\n\n\n", true); } else { publishProgress("\t" + context.getString(R.string.main_no_database_available), true); + running = false; } + running = false; return null; } @@ -139,6 +177,7 @@ class MalwareScanner extends AsyncTask, Object, String> { } private void checkSignature(String hashType, HashMap signaturesToCheck, HashMap signatureDatabase) { + //XXX: TODO: This is a map with hash,file meaning multiple files with the same hashes will only match once! if (signatureDatabase.size() > 0) { for (Map.Entry file : signaturesToCheck.entrySet()) { if (signatureDatabase.containsKey(file.getKey())) { diff --git a/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java b/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java index f053e32..bbca189 100644 --- a/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java +++ b/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java @@ -27,6 +27,8 @@ import android.os.Build; import android.os.Environment; import android.os.FileObserver; import android.os.IBinder; +import android.widget.Toast; + import androidx.core.app.NotificationCompat; import java.io.File; @@ -107,7 +109,7 @@ public class MalwareScannerService extends Service { } malwareMonitors.clear(); System.gc(); - //Toast.makeText(this, "Hypatia: Realtime Scanning Stopped", Toast.LENGTH_SHORT).show(); //TODO: Move to strings.xml + Toast.makeText(this, "Hypatia: Realtime Scanning Stopped", Toast.LENGTH_SHORT).show(); //TODO: Move to strings.xml } private void setForeground() { @@ -127,7 +129,7 @@ public class MalwareScannerService extends Service { } private void updateForegroundNotification() { - foregroundNotification.setSubText(getString(R.string.main_files_scanned_count, Utils.FILES_SCANNED) + ""); + foregroundNotification.setSubText(getString(R.string.main_files_scanned_count, Utils.FILES_SCANNED + "")); notificationManager.notify(-1, foregroundNotification.build()); } diff --git a/app/src/main/java/us/spotco/malwarescanner/Utils.java b/app/src/main/java/us/spotco/malwarescanner/Utils.java index 88026e0..d7021f2 100644 --- a/app/src/main/java/us/spotco/malwarescanner/Utils.java +++ b/app/src/main/java/us/spotco/malwarescanner/Utils.java @@ -37,7 +37,6 @@ class Utils { public final static int MAX_SCAN_SIZE_REALTIME = MAX_SCAN_SIZE / 2; //40MB public final static int MAX_HASH_LENGTH = 12; - public final static boolean TRIM_VARIANT_NUMBER = false; public static int FILES_SCANNED = 0; private static ThreadPoolExecutor threadPoolExecutor = null; @@ -51,8 +50,8 @@ class Utils { public static int getMaxThreads() { int maxTheads = Runtime.getRuntime().availableProcessors(); - if (maxTheads >= 2) { - maxTheads /= 2; + if (maxTheads > 4) { + maxTheads = 4; } return maxTheads; } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 77ef0c9..7803e5c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -22,8 +22,6 @@ Bekannte Malware wird in Echtzeit erkannt Echtzeit-Scanner - dd.MM.yyyy - Update von %s Datenbank(en) ... Download von %s Erfolgreich heruntergeladen diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8b1678a..d42084b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -23,8 +23,6 @@ Malware détecté: Scanner en temps réel - dd.MM.yyyy - Mise à jour des %s bases de données ... Télécharger %s Télédéchargement réussi diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e931622..5f21b41 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,5 @@ #4CAF50 #03a9f4 + #f44336 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d348fa0..8a5b7d2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,8 +23,6 @@ Malware Detected: Realtime Scanner - yyyy/MM/dd - Updating %s databases... Downloading %s Successfully downloaded @@ -36,12 +34,15 @@ Failed to download, check logcat Starting scan... + Cancelling scan... + Cancelled scan %s files pending scan + Loading database... Loaded database with %s signatures Hashing files... Calculated hashes for all files Checked all %s hashes against signature databases No %s hashes signatures available Scan completed in %s seconds! - % files scanned + %s files scanned diff --git a/graphics/hypatia-test-file.png b/graphics/hypatia-test-file.png new file mode 100644 index 0000000000000000000000000000000000000000..934f57d12eb82e57c4f771edc6c02178afcd49c0 GIT binary patch literal 13307 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 aUbrb({jOG3T!O#i0UFnJR7;d?pZ*UjD(lAp literal 0 HcmV?d00001 diff --git a/graphics/hypatia-test-file.xcf b/graphics/hypatia-test-file.xcf new file mode 100644 index 0000000000000000000000000000000000000000..8f167f97226c184c6e964f5480dd73deb1ea46bf GIT binary patch literal 42124 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 BL#zM* literal 0 HcmV?d00001