Merge pull request #1786 from MilosKozak/signature_verifier
Signature Verifier
This commit is contained in:
commit
c925072649
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.overview.OverviewPlugin;
|
||||
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.tidepool.TidepoolPlugin;
|
||||
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(VersionCheckerPlugin.INSTANCE);
|
||||
if (Config.SAFETY) pluginsList.add(StorageConstraintPlugin.getPlugin());
|
||||
if (Config.SAFETY) pluginsList.add(SignatureVerifier.getPlugin());
|
||||
if (Config.APS) pluginsList.add(ObjectivesPlugin.getPlugin());
|
||||
pluginsList.add(SourceXdripPlugin.getPlugin());
|
||||
pluginsList.add(SourceNSClientPlugin.getPlugin());
|
||||
|
|
|
@ -76,6 +76,7 @@ public class Notification {
|
|||
public static final int OLDVERSION = 52;
|
||||
public static final int USERMESSAGE = 53;
|
||||
public static final int OVER_24H_TIME_CHANGE_REQUESTED = 54;
|
||||
public static final int INVALID_VERSION = 55;
|
||||
|
||||
|
||||
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_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_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="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