GPG signature verification for databases

TODO: inform user if verification fails or key is unavailable

Signed-off-by: Tad <tad@spotco.us>
This commit is contained in:
Tad 2023-04-16 12:31:12 -04:00
parent 76d06b504f
commit 44d2d1c905
No known key found for this signature in database
GPG key ID: B286E9F57A07424B
17 changed files with 1326 additions and 628 deletions

2
.idea/compiler.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="17" />
</component>
</project>

2
.idea/misc.xml generated
View file

@ -54,7 +54,7 @@
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View file

@ -43,7 +43,6 @@ Geplante Updates
- Automatische Datenbank-Updates
- Automatische Datenbankerstellung
- Client-seitige Datenbank-Generierung
- Überprüfung der Datenbanksignatur
- Datenbank-Sanity-Checks
- Prüfung
- Bessere GUI

View file

@ -45,7 +45,6 @@ Planned Updates
- Automatic database updates
- Automatic database generation
- Client side database generation
- Database signature verification
- Database sanity checks
- Testing
- Better GUI
@ -66,7 +65,8 @@ Credits
- Nex (@botherder) for extra databases (CC BY-SA 4.0)
- Amnesty International for extra databases (CC BY 2.0)
- Echap for extra databases (CC BY 4.0)
- RecursiveFileObserver.java (GPLv3): Daniel Gultsch, ownCloud Inc., Bartek Przybylski
- RecursiveFileObserver.java (GPL-3.0-or-later): Daniel Gultsch, ownCloud Inc., Bartek Przybylski
- GPGDetachedSignatureVerifier.java (GPL-2.0-or-later): Federico Fissore, Arduino LLC
- Petra Mirelli for the German/Spanish/Italian translations, the app banner/feature graphic, and various tweaks.
- Jean-Luc Tibaux and Petra Mirelli for the French translations.
- @srccrow for the Italian translations.

View file

@ -43,7 +43,6 @@ Planowane aktualizacje
- Automatyczne aktualizacje baz danych
- Automatyczne generowanie baz danych
- Generowanie baz danych przez klienta
- Weryfikacja podpisów baz danych
- Kontrola poprawności baz danych
- Testy
- Lepsze GUI

View file

@ -6,8 +6,8 @@ android {
applicationId "us.spotco.malwarescanner"
minSdkVersion 16
targetSdkVersion 32
versionCode 96
versionName "2.30"
versionCode 97
versionName "2.31"
resConfigs 'en', 'af', 'de', 'es', 'fi', 'fr', 'it', 'pl', 'pt', 'ru'
}
buildTypes {
@ -29,5 +29,6 @@ android {
}
dependencies {
implementation 'commons-io:commons-io:2.11.0'
implementation 'org.bouncycastle:bcpg-jdk15to18:1.73'
}

View file

@ -20,6 +20,7 @@ package us.spotco.malwarescanner;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.TextView;
import java.io.BufferedReader;
@ -72,12 +73,15 @@ class Database {
public static void updateDatabase(Context context, ConcurrentLinkedQueue<SignatureDatabase> signatureDatabases) {
initDatabase(context);
log.append(context.getString(R.string.main_database_updating, signatureDatabases.size() + "") + "\n");
if(!Utils.getDatabaseURL(context).equals(Utils.DATABASE_URL_DEFAULT)) {
if (!Utils.getDatabaseURL(context).equals(Utils.DATABASE_URL_DEFAULT)) {
log.append(context.getString(R.string.main_database_override, Utils.getDatabaseURL(context)) + "\n");
}
boolean onionRouting = prefs.getBoolean("ONION_ROUTING", false);
new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, Utils.getDatabaseURL(context) + "gpg.key", databasePath + "/gpg.key", Utils.getDatabaseURL(context));
for (SignatureDatabase signatureDatabase : signatureDatabases) {
boolean onionRouting = prefs.getBoolean("ONION_ROUTING", false);
new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, signatureDatabase.getUrl(), databasePath + "/" + signatureDatabase.getName(), signatureDatabase.getBaseUrl());
new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, signatureDatabase.getUrl() + ".sig", databasePath + "/" + signatureDatabase.getName() + ".sig", signatureDatabase.getBaseUrl());
}
}
@ -126,39 +130,48 @@ class Database {
signaturesSHA1.clear();
signaturesSHA256.clear();
System.gc();
File publicKey = new File(databasePath + "/gpg.key");
GPGDetachedSignatureVerifier verifier = new GPGDetachedSignatureVerifier(Utils.getSigningKey(context));
for (SignatureDatabase database : signatureDatabases) {
File databaseLocation = new File(databasePath + "/" + database.getName());
if (databaseLocation.exists()) {
File databaseSigLocation = new File(databasePath + "/" + database.getName() + ".sig");
if (publicKey.exists() && databaseLocation.exists() && databaseSigLocation.exists()) {
try {
BufferedReader reader;
if (databaseLocation.getName().endsWith(".gz")) {
reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(databaseLocation))));
boolean validated = verifier.verify(databaseLocation, databaseSigLocation, publicKey);
if (validated) {
Log.d("Hypatia", "Successfully validated database");
BufferedReader reader;
if (databaseLocation.getName().endsWith(".gz")) {
reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(databaseLocation))));
} else {
reader = new BufferedReader(new FileReader(databaseLocation));
}
String line;
if (database.getName().contains(".hdb")) {//.hdb format: md5, size, name
while ((line = reader.readLine()) != null) {
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].intern());
}
}
}
} else if (database.getName().contains(".hsb")) {//.hsb format: sha256, size, name
while ((line = reader.readLine()) != null) {
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].intern());
} else if (lineS[0].length() > 0) {
signaturesSHA256.put(lineS[0].substring(0, Utils.MAX_HASH_LENGTH), lineS[2].intern());
}
}
}
}
reader.close();
} else {
reader = new BufferedReader(new FileReader(databaseLocation));
Log.w("Hypatia", "Failed to validate database");
}
String line;
if (database.getName().contains(".hdb")) {//.hdb format: md5, size, name
while ((line = reader.readLine()) != null) {
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].intern());
}
}
}
} else if (database.getName().contains(".hsb")) {//.hsb format: sha256, size, name
while ((line = reader.readLine()) != null) {
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].intern());
} else if (lineS[0].length() > 0) {
signaturesSHA256.put(lineS[0].substring(0, Utils.MAX_HASH_LENGTH), lineS[2].intern());
}
}
}
}
reader.close();
} catch (Exception e) {
e.printStackTrace();
}

View file

@ -0,0 +1,133 @@
/*
* Source: https://github.com/arduino/Arduino/blob/master/arduino-core/src/cc/arduino/contributions/GPGDetachedSignatureVerifier.java
* This file is part of Arduino.
*
* Copyright 2015 Arduino LLC (http://www.arduino.cc/)
*
* Arduino is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* As a special exception, you may use this file as part of a free software
* library without restriction. Specifically, if other files instantiate
* templates or use macros or inline functions from this file, or you compile
* this file and link it with other files to produce an executable, this
* file does not by itself cause the resulting executable to be covered by
* the GNU General Public License. This exception does not however
* invalidate any other reasons why the executable file might be covered by
* the GNU General Public License.
*/
package us.spotco.malwarescanner;
import android.util.Log;
import org.apache.commons.io.IOUtils;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
public class GPGDetachedSignatureVerifier {
private final String keyId;
public GPGDetachedSignatureVerifier() {
this(Utils.SIGNING_KEY_DEFAULT);
}
public GPGDetachedSignatureVerifier(String keyId) {
this.keyId = keyId;
}
public boolean verify(File signedFile, File signature, File publicKey) throws IOException {
FileInputStream signatureInputStream = null;
FileInputStream signedFileInputStream = null;
try {
signatureInputStream = new FileInputStream(signature);
PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(signatureInputStream, new BcKeyFingerprintCalculator());
Object nextObject;
try {
nextObject = pgpObjectFactory.nextObject();
if (!(nextObject instanceof PGPSignatureList)) {
return false;
}
} catch (IOException e) {
return false;
}
PGPSignatureList pgpSignatureList = (PGPSignatureList) nextObject;
assert pgpSignatureList.size() == 1;
PGPSignature pgpSignature = pgpSignatureList.get(0);
PGPPublicKey pgpPublicKey = readPublicKey(publicKey, keyId);
pgpSignature.init(new BcPGPContentVerifierBuilderProvider(), pgpPublicKey);
signedFileInputStream = new FileInputStream(signedFile);
pgpSignature.update(IOUtils.toByteArray(signedFileInputStream));
return pgpSignature.verify();
} catch (PGPException e) {
throw new IOException(e);
} finally {
if (signatureInputStream != null) {
signatureInputStream.close();
}
if (signedFileInputStream != null) {
signedFileInputStream.close();
}
}
}
private PGPPublicKey readPublicKey(File file, String id) throws IOException, PGPException {
try (InputStream keyIn = new BufferedInputStream(new FileInputStream(file))) {
return readPublicKey(keyIn, id);
}
}
private PGPPublicKey readPublicKey(InputStream input, String id) throws IOException, PGPException {
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(input), new BcKeyFingerprintCalculator());
Iterator<PGPPublicKeyRing> keyRingIter = pgpPub.getKeyRings();
while (keyRingIter.hasNext()) {
PGPPublicKeyRing keyRing = keyRingIter.next();
Iterator<PGPPublicKey> keyIter = keyRing.getPublicKeys();
while (keyIter.hasNext()) {
PGPPublicKey key = keyIter.next();
if (Long.toHexString(key.getKeyID()).toUpperCase().endsWith(id)) {
return key;
} else {
Log.d("Hypatia", "readPublicKey: No match found, have key: " + Long.toHexString(key.getKeyID()).toUpperCase());
}
}
}
throw new IllegalArgumentException("Can't find encryption key in key ring.");
}
}

View file

@ -24,7 +24,6 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
@ -199,7 +198,7 @@ public class MainActivity extends Activity {
}
break;
case R.id.mnuUpdateDatabase:
if(malwareScanner.running) {
if (malwareScanner.running) {
logView.append(getString(R.string.lblScanRunning) + "\n");
} else {
if (prefs.getBoolean("ONION_ROUTING", false)) {
@ -213,33 +212,41 @@ public class MainActivity extends Activity {
selectDatabases();
break;
case R.id.mnuDatabaseServer:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.lblDatabaseServer));
final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(Utils.getDatabaseURL(this));
builder.setView(input);
builder.setPositiveButton(getString(R.string.lblOverride), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String newServer = input.getText().toString();
if(!newServer.endsWith("/")) {
newServer += "/";
}
prefs.edit().putString("DATABASE_SERVER", newServer).apply();
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();
});
builder.setNegativeButton(getString(R.string.lblReset), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
prefs.edit().putString("DATABASE_SERVER", Utils.DATABASE_URL_DEFAULT).apply();
dialog.cancel();
}
builderServerOverride.setNegativeButton(getString(R.string.lblReset), (dialog, which) -> {
prefs.edit().putString("DATABASE_SERVER", Utils.DATABASE_URL_DEFAULT).apply();
dialog.cancel();
});
builder.show();
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();
break;
case R.id.toggleRealtime:
if(malwareScanner.running) {
if (malwareScanner.running) {
logView.append(getString(R.string.lblScanRunning) + "\n");
} else {
Intent realtimeScanner = new Intent(getApplicationContext(), MalwareScannerService.class);
@ -308,7 +315,7 @@ public class MainActivity extends Activity {
malwareScanner.executeOnExecutor(Utils.getThreadPoolExecutor(), filesToScan);
new Thread(() -> {
try {
while(malwareScanner.running){
while (malwareScanner.running) {
Thread.sleep(500);
}
runOnUiThread(() -> updateScanButton(false));
@ -319,7 +326,7 @@ public class MainActivity extends Activity {
}
private void updateDatabase() {
new Database((TextView) findViewById(R.id.txtLogOutput));
new Database(findViewById(R.id.txtLogOutput));
Database.updateDatabase(this, Database.signatureDatabases);
if (Database.isDatabaseLoaded()) {
Utils.getThreadPoolExecutor().execute(() -> Database.loadDatabase(getApplicationContext(), true, Database.signatureDatabases));
@ -327,7 +334,7 @@ public class MainActivity extends Activity {
}
private void updateScanButton(boolean running) {
if(menu == null || menu.findItem(R.id.btnStartScan) == null) {
if (menu == null || menu.findItem(R.id.btnStartScan) == null) {
return;
}
if (running) {

View file

@ -35,6 +35,8 @@ class SignatureDatabase {
return name;
}
public final String getUrl() { return baseURL + name; }
public final String getUrl() {
return baseURL + name;
}
}

View file

@ -38,6 +38,7 @@ class Utils {
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 String DATABASE_URL_DEFAULT = "https://divested.dev/MalwareScannerSignatures/";
public final static String SIGNING_KEY_DEFAULT = "BADFCABDDBF5B694";
public final static int MAX_HASH_LENGTH = 12;
@ -60,7 +61,7 @@ class Utils {
if (maxThreads > 4) {
maxThreads = 4;
}
if(maxThreads < 2) {
if (maxThreads < 2) {
maxThreads = 2;
}
return maxThreads;
@ -104,6 +105,11 @@ class Utils {
return prefs.getString("DATABASE_SERVER", DATABASE_URL_DEFAULT);
}
public static String getSigningKey(Context context) {
SharedPreferences prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
return prefs.getString("SIGNING_KEY", SIGNING_KEY_DEFAULT);
}
public static void considerStartService(Context context) {
if (!Utils.isServiceRunning(MalwareScannerService.class, context)) {
SharedPreferences prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);

View file

@ -20,6 +20,9 @@
<item
android:id="@+id/mnuDatabaseServer"
android:title="@string/lblDatabaseServer" />
<item
android:id="@+id/mnuSigningKey"
android:title="@string/lblSigningKey" />
<item
android:id="@+id/toggleRealtime"
android:title="@string/lblRealtimeScannerToggle"

View file

@ -61,4 +61,5 @@
<string name="db_desc_size_large">Large</string>
<string name="scan_control">Scan Control</string>
<string name="lblScanRunning">Skipping action, a scan is running!</string>
<string name="lblSigningKey">Database signing key</string>
</resources>

View file

@ -0,0 +1,4 @@
* Databases are now verified using GPG signatures
* Users must use "Update databases" before use after installing this update
* Databases that are not signed or fail to verify will be ignored
* A custom database key is allowed to maintain support for third party database repos

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -60,6 +60,7 @@
<trusted-key id="aa70c7c433d501636392ec02153e7a3c2b4e5118" group="org.eclipse.ee4j" name="project"/>
<trusted-key id="afcc4c7594d09e2182c60e0f7a01b0f236e5430f" group="com.google.code.gson"/>
<trusted-key id="b02335aa54ccf21e52bbf9abd9c565aa72ba2fdd" group="io.grpc"/>
<trusted-key id="b6e73d84ea4fcc47166087253faad2cd5ecbb314" group="org.apache.commons" name="commons-parent"/>
<trusted-key id="b801e2f8ef035068ec1139cc29579f18fa8fd93b" group="com.google.j2objc" name="j2objc-annotations" version="1.3"/>
<trusted-key id="bcc135fc7ed8214f823d73e97fe9900f412d622e" group="com.google.flatbuffers" name="flatbuffers-java" version="1.12.0"/>
<trusted-key id="bdb5fa4fe719d787fb3d3197f6d4a1d411e9d1ae" group="com.google.guava"/>
@ -72,6 +73,7 @@
<trusted-key id="ee0ca873074092f806f59b65d364abaa39a47320" group="com.google.errorprone"/>
<trusted-key id="f254b35617dc255d9344bcfa873a8e86b4372146" group="org.codehaus.mojo"/>
<trusted-key id="f3184bcd55f4d016e30d4c9bf42e87f9665015c9" group="org.jsoup" name="jsoup" version="1.13.1"/>
<trusted-key id="fa77dcfef2ee6eb2debedd2c012579464d01c06a" group="org.apache" name="apache"/>
<trusted-key id="fa7929f83ad44c4590f6cc6815c71c0a4e0b8edd" group="net.java.dev.jna"/>
<trusted-key id="fc411cd3cb7dcb0abc9801058118b3bcdb1a5000" group="jakarta.xml.bind"/>
</trusted-keys>
@ -1155,6 +1157,11 @@
<sha512 value="c675dc20d3d192a4193d651a6fa3ddac3bfe97844be536146ca6e78c29c1559b06fe9495be39d4dbf606b8a2cb0720391a6fe53f37d628949659c3224e4eaa8d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.junit" name="junit-bom" version="5.7.2">
<artifact name="junit-bom-5.7.2.pom">
<pgp value="ff6e2c001948c5f2f38b0cc385911f425ec61b51"/>
</artifact>
</component>
<component group="org.ow2" name="ow2" version="1.5">
<artifact name="ow2-1.5.pom">
<sha512 value="5445748e294cf9f23fe8f1e18e2ebb7108800d40f81a4566a73f9434fe21d2058d05acf3bc4d15f629151df47c42bcf948de3bba0b6a37982dfc3a8f1baf244d" origin="Generated by Gradle because artifact wasn't signed"/>