diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index b96a427..2324abe 100644 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/app/build.gradle b/app/build.gradle index 779b35e..5173b66 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "us.spotco.malwarescanner" minSdkVersion 16 targetSdkVersion 30 - versionCode 55 - versionName "2.14" + versionCode 56 + versionName "2.15" resConfigs "en", "de", "fr" } buildTypes { diff --git a/app/src/main/java/us/spotco/malwarescanner/Database.java b/app/src/main/java/us/spotco/malwarescanner/Database.java index cb85541..ae9dd73 100644 --- a/app/src/main/java/us/spotco/malwarescanner/Database.java +++ b/app/src/main/java/us/spotco/malwarescanner/Database.java @@ -33,7 +33,6 @@ import java.net.InetSocketAddress; import java.net.Proxy; import java.net.URL; import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -47,6 +46,7 @@ class Database { private static SharedPreferences prefs = null; private static File databasePath = null; private static ThreadPoolExecutor threadPoolExecutor = null; + private static boolean databaseFullyLoaded = false; public final static HashSet 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 0000000..934f57d Binary files /dev/null and b/graphics/hypatia-test-file.png differ diff --git a/graphics/hypatia-test-file.xcf b/graphics/hypatia-test-file.xcf new file mode 100644 index 0000000..8f167f9 Binary files /dev/null and b/graphics/hypatia-test-file.xcf differ