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
This commit is contained in:
Tad 2021-02-22 11:04:05 -05:00
parent e1bc93fbe0
commit eccaaadd5d
13 changed files with 122 additions and 52 deletions

Binary file not shown.

View file

@ -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 {

View file

@ -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<SignatureDatabase> signatureDatabases = new HashSet<>();
public final static String baseURL = "https://divested.dev/MalwareScannerSignatures/";
@ -56,7 +56,7 @@ class Database {
public final static HashMap<String, String> signaturesSHA1 = new HashMap<>();
public final static HashMap<String, String> 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<SignatureDatabase> 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<SignatureDatabase> 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);

View file

@ -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);
}
});
}
}

View file

@ -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<Set<File>, Object, String> {
private final HashMap<String, File> fileHashesMD5 = new HashMap<>();
private final HashMap<String, File> fileHashesSHA1 = new HashMap<>();
private final HashMap<String, File> 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<Set<File>, 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<Set<File>, 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<Set<File>, Object, String> {
@Override
protected final String doInBackground(Set<File>[] filesToScan) {
running = true;
//Pre
fileHashesMD5.clear();
fileHashesSHA1.clear();
@ -105,15 +110,46 @@ class MalwareScanner extends AsyncTask<Set<File>, 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<Set<File>, 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<Set<File>, Object, String> {
}
private void checkSignature(String hashType, HashMap<String, File> signaturesToCheck, HashMap<String, String> 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<String, File> file : signaturesToCheck.entrySet()) {
if (signatureDatabase.containsKey(file.getKey())) {

View file

@ -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());
}

View file

@ -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;
}

View file

@ -22,8 +22,6 @@
<string name="lblNotificationRealtimeText">Bekannte Malware wird in Echtzeit erkannt</string>
<string name="lblRealtimeScannerToggle">Echtzeit-Scanner</string>
<string name="simple_date_format_short">dd.MM.yyyy</string>
<string name="main_database_updating">Update von %s Datenbank(en) ...</string>
<string name="main_database_downloading">Download von %s</string>
<string name="main_database_download_success">Erfolgreich heruntergeladen</string>

View file

@ -23,8 +23,6 @@
<string name="lblNotificationRealtimeDetection">Malware détecté:</string>
<string name="lblRealtimeScannerToggle">Scanner en temps réel</string>
<string name="simple_date_format_short">dd.MM.yyyy</string>
<string name="main_database_updating">Mise à jour des %s bases de données ...</string>
<string name="main_database_downloading">Télécharger %s</string>
<string name="main_database_download_success">Télédéchargement réussi</string>

View file

@ -2,4 +2,5 @@
<resources>
<color name="colorPrimary">#4CAF50</color>
<color name="light_blue">#03a9f4</color>
<color name="red">#f44336</color>
</resources>

View file

@ -23,8 +23,6 @@
<string name="lblNotificationRealtimeDetection">Malware Detected:</string>
<string name="lblRealtimeScannerToggle">Realtime Scanner</string>
<string name="simple_date_format_short">yyyy/MM/dd</string>
<string name="main_database_updating">Updating %s databases...</string>
<string name="main_database_downloading">Downloading %s</string>
<string name="main_database_download_success">Successfully downloaded</string>
@ -36,12 +34,15 @@
<string name="main_database_download_error_logcat">Failed to download, check logcat</string>
<string name="main_starting_scan">Starting scan...</string>
<string name="main_cancelling_scan">Cancelling scan...</string>
<string name="main_cancelled_scan">Cancelled scan</string>
<string name="main_files_pending_scan">%s files pending scan</string>
<string name="main_database_loading">Loading database...</string>
<string name="main_database_loaded">Loaded database with %s signatures</string>
<string name="main_hashing_files">Hashing files...</string>
<string name="main_hashing_done">Calculated hashes for all files</string>
<string name="main_hash_scan_done">Checked all %s hashes against signature databases</string>
<string name="main_no_hashes_available">No %s hashes signatures available</string>
<string name="main_scanning_done">Scan completed in %s seconds!</string>
<string name="main_files_scanned_count">% files scanned</string>
<string name="main_files_scanned_count">%s files scanned</string>
</resources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.