Merge pull request #1786 from MilosKozak/signature_verifier
Signature Verifier
This commit is contained in:
commit
c925072649
7 changed files with 231 additions and 0 deletions
2
app/src/main/assets/revoked_certs.txt
Normal file
2
app/src/main/assets/revoked_certs.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
#Demo certificate
|
||||||
|
51:6D:12:67:4C:27:F4:9B:9F:E5:42:9B:01:B3:98:E4:66:2B:85:B7:A8:DD:70:32:B7:6A:D7:97:9A:0D:97:10
|
|
@ -56,6 +56,7 @@ import info.nightscout.androidaps.plugins.general.nsclient.receivers.AckAlarmRec
|
||||||
import info.nightscout.androidaps.plugins.general.nsclient.receivers.DBAccessReceiver;
|
import info.nightscout.androidaps.plugins.general.nsclient.receivers.DBAccessReceiver;
|
||||||
import info.nightscout.androidaps.plugins.general.overview.OverviewPlugin;
|
import info.nightscout.androidaps.plugins.general.overview.OverviewPlugin;
|
||||||
import info.nightscout.androidaps.plugins.general.persistentNotification.PersistentNotificationPlugin;
|
import info.nightscout.androidaps.plugins.general.persistentNotification.PersistentNotificationPlugin;
|
||||||
|
import info.nightscout.androidaps.plugins.general.signatureVerifier.SignatureVerifier;
|
||||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin;
|
import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin;
|
||||||
import info.nightscout.androidaps.plugins.general.tidepool.TidepoolPlugin;
|
import info.nightscout.androidaps.plugins.general.tidepool.TidepoolPlugin;
|
||||||
import info.nightscout.androidaps.plugins.general.versionChecker.VersionCheckerPlugin;
|
import info.nightscout.androidaps.plugins.general.versionChecker.VersionCheckerPlugin;
|
||||||
|
@ -202,6 +203,7 @@ public class MainApp extends Application {
|
||||||
if (Config.SAFETY) pluginsList.add(SafetyPlugin.getPlugin());
|
if (Config.SAFETY) pluginsList.add(SafetyPlugin.getPlugin());
|
||||||
if (Config.SAFETY) pluginsList.add(VersionCheckerPlugin.INSTANCE);
|
if (Config.SAFETY) pluginsList.add(VersionCheckerPlugin.INSTANCE);
|
||||||
if (Config.SAFETY) pluginsList.add(StorageConstraintPlugin.getPlugin());
|
if (Config.SAFETY) pluginsList.add(StorageConstraintPlugin.getPlugin());
|
||||||
|
if (Config.SAFETY) pluginsList.add(SignatureVerifier.getPlugin());
|
||||||
if (Config.APS) pluginsList.add(ObjectivesPlugin.getPlugin());
|
if (Config.APS) pluginsList.add(ObjectivesPlugin.getPlugin());
|
||||||
pluginsList.add(SourceXdripPlugin.getPlugin());
|
pluginsList.add(SourceXdripPlugin.getPlugin());
|
||||||
pluginsList.add(SourceNSClientPlugin.getPlugin());
|
pluginsList.add(SourceNSClientPlugin.getPlugin());
|
||||||
|
|
|
@ -76,6 +76,7 @@ public class Notification {
|
||||||
public static final int OLDVERSION = 52;
|
public static final int OLDVERSION = 52;
|
||||||
public static final int USERMESSAGE = 53;
|
public static final int USERMESSAGE = 53;
|
||||||
public static final int OVER_24H_TIME_CHANGE_REQUESTED = 54;
|
public static final int OVER_24H_TIME_CHANGE_REQUESTED = 54;
|
||||||
|
public static final int INVALID_VERSION = 55;
|
||||||
|
|
||||||
|
|
||||||
public int id;
|
public int id;
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
package info.nightscout.androidaps.plugins.general.signatureVerifier;
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.Signature;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.spongycastle.util.encoders.Hex;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import info.nightscout.androidaps.MainApp;
|
||||||
|
import info.nightscout.androidaps.R;
|
||||||
|
import info.nightscout.androidaps.interfaces.Constraint;
|
||||||
|
import info.nightscout.androidaps.interfaces.ConstraintsInterface;
|
||||||
|
import info.nightscout.androidaps.interfaces.PluginBase;
|
||||||
|
import info.nightscout.androidaps.interfaces.PluginDescription;
|
||||||
|
import info.nightscout.androidaps.interfaces.PluginType;
|
||||||
|
import info.nightscout.androidaps.logging.L;
|
||||||
|
import info.nightscout.androidaps.plugins.general.overview.events.EventNewNotification;
|
||||||
|
import info.nightscout.androidaps.plugins.general.overview.notifications.Notification;
|
||||||
|
import info.nightscout.androidaps.utils.SP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AndroidAPS is meant to be build by the user.
|
||||||
|
* In case someone decides to leak a ready-to-use APK nonetheless, we can still disable it.
|
||||||
|
* Self-compiled APKs with privately held certificates cannot and will not be disabled.
|
||||||
|
*/
|
||||||
|
public class SignatureVerifier extends PluginBase implements ConstraintsInterface {
|
||||||
|
|
||||||
|
private static final String REVOKED_CERTS_URL = "https://raw.githubusercontent.com/MilosKozak/AndroidAPS/master/app/src/main/assets/revoked_certs.txt";
|
||||||
|
private static final long UPDATE_INTERVAL = TimeUnit.DAYS.toMillis(1);
|
||||||
|
|
||||||
|
private static SignatureVerifier plugin = new SignatureVerifier();
|
||||||
|
|
||||||
|
private Logger log = LoggerFactory.getLogger(L.CORE);
|
||||||
|
private final Object $lock = new Object[0];
|
||||||
|
private File revokedCertsFile;
|
||||||
|
private List<byte[]> revokedCerts;
|
||||||
|
|
||||||
|
public static SignatureVerifier getPlugin() {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignatureVerifier() {
|
||||||
|
super(new PluginDescription()
|
||||||
|
.mainType(PluginType.CONSTRAINTS)
|
||||||
|
.neverVisible(true)
|
||||||
|
.alwaysEnabled(true)
|
||||||
|
.showInList(false)
|
||||||
|
.pluginName(R.string.signature_verifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
revokedCertsFile = new File(MainApp.instance().getFilesDir(), "revoked_certs.txt");
|
||||||
|
new Thread(() -> {
|
||||||
|
loadLocalRevokedCerts();
|
||||||
|
if (shouldDownloadCerts()) {
|
||||||
|
try {
|
||||||
|
downloadAndSaveRevokedCerts();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Could not download revoked certs", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasIllegalSignature()) showNotification();
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Constraint<Boolean> isLoopInvocationAllowed(Constraint<Boolean> value) {
|
||||||
|
if (hasIllegalSignature()) {
|
||||||
|
showNotification();
|
||||||
|
value.set(false);
|
||||||
|
}
|
||||||
|
if (shouldDownloadCerts()) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
downloadAndSaveRevokedCerts();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Could not download revoked certs", e);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showNotification() {
|
||||||
|
Notification notification = new Notification(Notification.INVALID_VERSION, MainApp.gs(R.string.running_invalid_version), Notification.URGENT);
|
||||||
|
MainApp.bus().post(new EventNewNotification(notification));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasIllegalSignature() {
|
||||||
|
try {
|
||||||
|
synchronized ($lock) {
|
||||||
|
if (revokedCerts == null) return false;
|
||||||
|
Signature[] signatures = MainApp.instance().getPackageManager().getPackageInfo(MainApp.instance().getPackageName(), PackageManager.GET_SIGNATURES).signatures;
|
||||||
|
for (Signature signature : signatures) {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA256");
|
||||||
|
byte[] fingerprint = digest.digest(signature.toByteArray());
|
||||||
|
for (byte[] cert : revokedCerts) {
|
||||||
|
if (Arrays.equals(cert, fingerprint)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
log.error("Error in SignatureVerifier", e);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
log.error("Error in SignatureVerifier", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldDownloadCerts() {
|
||||||
|
return System.currentTimeMillis() - SP.getLong(R.string.key_last_revoked_certs_check, 0L) >= UPDATE_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void downloadAndSaveRevokedCerts() throws IOException {
|
||||||
|
String download = downloadRevokedCerts();
|
||||||
|
saveRevokedCerts(download);
|
||||||
|
SP.putLong(R.string.key_last_revoked_certs_check, System.currentTimeMillis());
|
||||||
|
synchronized ($lock) {
|
||||||
|
revokedCerts = parseRevokedCertsFile(download);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadLocalRevokedCerts() {
|
||||||
|
try {
|
||||||
|
String revokedCerts = readCachedDownloadedRevokedCerts();
|
||||||
|
if (revokedCerts == null) revokedCerts = readRevokedCertsInAssets();
|
||||||
|
synchronized ($lock) {
|
||||||
|
this.revokedCerts = parseRevokedCertsFile(revokedCerts);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error in SignatureVerifier", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveRevokedCerts(String revokedCerts) throws IOException {
|
||||||
|
OutputStream outputStream = new FileOutputStream(revokedCertsFile);
|
||||||
|
outputStream.write(revokedCerts.getBytes(StandardCharsets.UTF_8));
|
||||||
|
outputStream.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String downloadRevokedCerts() throws IOException {
|
||||||
|
URLConnection connection = new URL(REVOKED_CERTS_URL).openConnection();
|
||||||
|
return readInputStream(connection.getInputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readInputStream(InputStream inputStream) throws IOException {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int read;
|
||||||
|
while ((read = inputStream.read(buffer)) != -1) {
|
||||||
|
baos.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
baos.flush();
|
||||||
|
return new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
} finally {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readRevokedCertsInAssets() throws IOException {
|
||||||
|
InputStream inputStream = MainApp.instance().getAssets().open("revoked_certs.txt");
|
||||||
|
return readInputStream(inputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readCachedDownloadedRevokedCerts() throws IOException {
|
||||||
|
if (!revokedCertsFile.exists()) return null;
|
||||||
|
return readInputStream(new FileInputStream(revokedCertsFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<byte[]> parseRevokedCertsFile(String file) {
|
||||||
|
List<byte[]> revokedCerts = new ArrayList<>();
|
||||||
|
for (String line : file.split("\n")) {
|
||||||
|
if (line.startsWith("#")) continue;
|
||||||
|
revokedCerts.add(Hex.decode(line.replace(" ", "").replace(":", "")));
|
||||||
|
}
|
||||||
|
return revokedCerts;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1427,6 +1427,9 @@
|
||||||
<string name="key_last_time_this_version_detected" translatable="false">last_time_this_version_detected</string>
|
<string name="key_last_time_this_version_detected" translatable="false">last_time_this_version_detected</string>
|
||||||
<string name="key_last_versionchecker_warning" translatable="false">last_versionchecker_waring</string>
|
<string name="key_last_versionchecker_warning" translatable="false">last_versionchecker_waring</string>
|
||||||
<string name="key_last_versionchecker_plugin_warning" translatable="false">last_versionchecker_plugin_waring</string>
|
<string name="key_last_versionchecker_plugin_warning" translatable="false">last_versionchecker_plugin_waring</string>
|
||||||
|
<string name="key_last_revoked_certs_check" translatable="false">last_revoked_certs_check</string>
|
||||||
|
<string name="signature_verifier">Signature verifier</string>
|
||||||
|
<string name="running_invalid_version">We have detected that you are running an invalid version. Loop disabled!</string>
|
||||||
|
|
||||||
<string name="old_version">old version</string>
|
<string name="old_version">old version</string>
|
||||||
<string name="very_old_version">very old version</string>
|
<string name="very_old_version">very old version</string>
|
||||||
|
|
BIN
demo_keystore.jks
Normal file
BIN
demo_keystore.jks
Normal file
Binary file not shown.
22
revoking_leaked_apks.md
Normal file
22
revoking_leaked_apks.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
## Revoking leaked APKs
|
||||||
|
In order to revoke a leaked APK, you need to retrieve the certificate first. This can be done by extracting the file ``META-INF\CERT.RSA`` from the APK. Open a terminal and run ``keytool -printcert -file CERT.RSA`` to get the SHA-256 fingerprint. The ``keytool`` utility is part of every JDK installation.
|
||||||
|
```
|
||||||
|
> keytool -printcert -file CERT.RSA
|
||||||
|
Owner: O=AndroidAPS
|
||||||
|
Issuer: O=AndroidAPS
|
||||||
|
Serial number: 30546c5b
|
||||||
|
Valid from: Wed May 01 16:37:40 CEST 2019 until: Sun Apr 24 16:37:40 CEST 2044
|
||||||
|
Certificate fingerprints:
|
||||||
|
SHA1: C4:EF:80:AD:CD:07:6F:28:B6:2E:8C:AE:C5:54:19:39:2E:E5:15:0D
|
||||||
|
SHA256: 51:6D:12:67:4C:27:F4:9B:9F:E5:42:9B:01:B3:98:E4:66:2B:85:B7:A8:DD:70:32:B7:6A:D7:97:9A:0D:97:10
|
||||||
|
Signature algorithm name: SHA256withRSA
|
||||||
|
Subject Public Key Algorithm: 2048-bit RSA key
|
||||||
|
Version: 3
|
||||||
|
```
|
||||||
|
Now revoke the certificate by attaching the SHA-256 checksum to ``app/src/main/assets/revoked_certs.txt`` and prepending a comment (starting with ``#``). Finally, push the changes to ``master`` branch to populate them.
|
||||||
|
```
|
||||||
|
#Demo certificate
|
||||||
|
51:6D:12:67:4C:27:F4:9B:9F:E5:42:9B:01:B3:98:E4:66:2B:85:B7:A8:DD:70:32:B7:6A:D7:97:9A:0D:97:10
|
||||||
|
````
|
||||||
|
### Demo keystore
|
||||||
|
You can verify this works by signing an APK with the demo keystore. The password for both the keystore and the key is ``androidaps``.
|
Loading…
Reference in a new issue