diff --git a/app/src/main/assets/revoked_certs.txt b/app/src/main/assets/revoked_certs.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index 3259483033..1aa204d01d 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -49,6 +49,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.versionChecker.VersionCheckerPlugin; import info.nightscout.androidaps.plugins.general.wear.WearPlugin; @@ -187,6 +188,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()); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java index f7a4c32360..ac8f31a762 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/notifications/Notification.java @@ -75,6 +75,7 @@ public class Notification { public static final int DST_IN_24H = 50; public static final int DISKFULL = 51; public static final int OLDVERSION = 52; + public static final int INVALID_VERSION = 53; public int id; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/signatureVerifier/SignatureVerifier.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/signatureVerifier/SignatureVerifier.java new file mode 100644 index 0000000000..c79bbe1277 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/signatureVerifier/SignatureVerifier.java @@ -0,0 +1,205 @@ +package info.nightscout.androidaps.plugins.general.signatureVerifier; + +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.provider.Settings; + +import com.j256.ormlite.stmt.query.In; + +import org.mozilla.javascript.tools.jsc.Main; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongycastle.util.encoders.Hex; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +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; + +/** + * For legal versions, AndroidAPS is meant to build by the user. In case someone decides to publish a ready-to-use APK nonetheless, we can still disable it. + */ +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 = 24 * 60 * 60 * 1000; + + 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 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 isLoopInvocationAllowed(Constraint 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 { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int read; + while ((read = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, read); + } + baos.flush(); + inputStream.close(); + return new String(baos.toByteArray(), StandardCharsets.UTF_8); + } + + 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 parseRevokedCertsFile(String file) { + List revokedCerts = new ArrayList<>(); + for (String line : file.split("\n")) { + if (line.startsWith("#")) continue; + revokedCerts.add(Hex.decode(line.replace(" ", ""))); + } + return revokedCerts; + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9620b64a50..7649317cc1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1330,6 +1330,9 @@ last_time_this_version_detected last_versionchecker_waring last_versionchecker_plugin_waring + last_revoked_certs_check + Signature verifier + We have detected that you are running an invalid version. Loop disabled! old version very old version