From f8daccffc061e2f05f6605913c19d4aa807eaddb Mon Sep 17 00:00:00 2001 From: cyBerta Date: Mon, 9 Nov 2020 15:37:31 +0100 Subject: initial auto-update implementation: introducing fatweb flavor, pgpverify go library and bitmask core library, basic update mechanism --- app/build.gradle | 71 +++- app/src/fatweb/AndroidManifest.xml | 62 +++ app/src/fatweb/assets/public.pgp | 458 +++++++++++++++++++++ .../appUpdate/DownloadBroadcastReceiver.java | 114 +++++ .../appUpdate/DownloadConnector.java | 116 ++++++ .../appUpdate/DownloadNotificationManager.java | 113 +++++ .../appUpdate/DownloadService.java | 82 ++++ .../appUpdate/DownloadServiceCommand.java | 81 ++++ .../appUpdate/FileProviderUtil.java | 52 +++ .../appUpdate/UpdateDownloadManager.java | 231 +++++++++++ .../java/se/leap/bitmaskclient/BitmaskApp.java | 38 +- .../se/leap/bitmaskclient/ButterKnifeActivity.java | 16 + .../main/java/se/leap/bitmaskclient/Constants.java | 20 + .../se/leap/bitmaskclient/EipSetupObserver.java | 21 + .../leap/bitmaskclient/OkHttpClientGenerator.java | 50 ++- .../java/se/leap/bitmaskclient/ProviderAPI.java | 2 +- .../se/leap/bitmaskclient/utils/ConfigHelper.java | 28 +- .../se/leap/bitmaskclient/utils/FileHelper.java | 24 ++ app/src/main/res/values/strings.xml | 6 + app/src/main/res/xml/file_provider_paths.xml | 12 + .../appUpdate/DownloadBroadcastReceiver.java | 18 + .../appUpdate/DownloadServiceCommand.java | 19 + bitmask-core/bitmask-core-sources.jar | Bin 0 -> 6880 bytes bitmask-core/bitmask-core.aar | Bin 0 -> 9015389 bytes bitmask-core/build.gradle | 2 + bitmask-web-core/bitmask-web-core-sources.jar | Bin 0 -> 8721 bytes bitmask-web-core/bitmask-web-core.aar | Bin 0 -> 10634042 bytes bitmask-web-core/build.gradle | 37 ++ build_deps.sh | 5 +- go/android_build_core.sh | 22 + go/android_build_shapeshifter_lib.sh | 21 - go/android_build_web_core.sh | 22 + .../se.leap.bitmaskclient/pgpverify/pgpverify.go | 82 ++++ settings.gradle | 2 +- shapeshifter/build.gradle | 2 - shapeshifter/shapeshifter-sources.jar | Bin 6880 -> 0 bytes shapeshifter/shapeshifter.aar | Bin 8497908 -> 0 bytes 37 files changed, 1780 insertions(+), 49 deletions(-) create mode 100644 app/src/fatweb/AndroidManifest.xml create mode 100644 app/src/fatweb/assets/public.pgp create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadService.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadServiceCommand.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/FileProviderUtil.java create mode 100644 app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java create mode 100644 app/src/main/res/xml/file_provider_paths.xml create mode 100644 app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadBroadcastReceiver.java create mode 100644 app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadServiceCommand.java create mode 100644 bitmask-core/bitmask-core-sources.jar create mode 100644 bitmask-core/bitmask-core.aar create mode 100644 bitmask-core/build.gradle create mode 100644 bitmask-web-core/bitmask-web-core-sources.jar create mode 100644 bitmask-web-core/bitmask-web-core.aar create mode 100644 bitmask-web-core/build.gradle create mode 100755 go/android_build_core.sh delete mode 100755 go/android_build_shapeshifter_lib.sh create mode 100755 go/android_build_web_core.sh create mode 100644 go/src/se.leap.bitmaskclient/pgpverify/pgpverify.go delete mode 100644 shapeshifter/build.gradle delete mode 100644 shapeshifter/shapeshifter-sources.jar delete mode 100644 shapeshifter/shapeshifter.aar diff --git a/app/build.gradle b/app/build.gradle index 7134e543..0b3403ff 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,6 @@ android { //This is the default donation URL and should be set to the donation page of LEAP // and this should not be set/altered anywhere else. buildConfigField 'String', 'default_donation_url', '"https://leap.se/en/about-us/donate"' - //This is the donation URL and should be set to the relevant donation page. - buildConfigField 'String', 'donation_url', 'null' //The field to enable donations in the app. buildConfigField 'boolean', 'enable_donation', 'true' //The field to enable donation reminder popup in the app if enable_donation is set to 'false' this will be disabled. @@ -37,7 +35,16 @@ android { buildConfigField 'int', 'donation_reminder_duration', '30' //skip the account creation / login screen if the provider offers anonymous vpn usage, use directly the anonymous cert instead buildConfigField 'boolean', 'priotize_anonymous_usage', 'false' + + // static update url pointing to the latest stable release apk + buildConfigField "String", "update_apk_url", '"https://dl.bitmask.net/client/android/Bitmask-Android-latest.apk"' + // the the pgp signature file of the apk + buildConfigField "String", "signature_url", '"https://dl.bitmask.net/client/android/Bitmask-Android-latest.apk.sig"' + // the version file contains the versionCode of the latest release + buildConfigField "String", "version_file_url", '"https://dl.bitmask.net/client/android/versioncode.txt"' + //ignore the following configs, only used in custom flavor + buildConfigField 'String', 'donation_url', 'null' buildConfigField "String", "customProviderUrl", '""' buildConfigField "String", "customProviderIp", '""' buildConfigField "String", "customProviderApiIp", '""' @@ -108,6 +115,17 @@ android { //skip the account creation / login screen if the provider offers anonymous vpn usage, use directly the anonymous cert instead buildConfigField 'boolean', 'priotize_anonymous_usage', 'true' + //Build Config Fields for automatic apk update checks + + // static update url pointing to the latest stable release apk + def apkURL = '"https://dl.bitmask.net/RiseupVPN/android/RiseupVPN-Android-latest.apk"' + buildConfigField "String", "update_apk_url", apkURL + // the the pgp signature file of the apk + def signatureURL = '"https://dl.bitmask.net/RiseupVPN/android/RiseupVPN-Android-latest.apk.sig"' + buildConfigField "String", "signature_url", signatureURL + // the version file should contain a single line with the versionCode of the latest release + buildConfigField "String", "version_file_url", '"https://dl.bitmask.net/client/android/versioncode.txt"' //'"https://dl.bitmask.net/RiseupVPN/android/versioncode.txt"' + //Build Config Fields for default donation details //This is the donation URL and should be set to the relevant donation page. @@ -130,6 +148,13 @@ android { } + fatweb { + dimension "abi" + ext { + abiVersionCode = 0 + abiFilter = "" + } + } fat { dimension "abi" @@ -232,6 +257,31 @@ android { androidTest { java.srcDirs += ['src/sharedTest/java'] } + + fatweb { + java.srcDirs += ['src/fatweb/java'] + } + + fat { + java.srcDirs += ['src/notFatweb/java'] + } + + x86 { + java.srcDirs += ['src/notFatweb/java'] + } + + x86_64 { + java.srcDirs += ['src/notFatweb/java'] + } + + armv7 { + java.srcDirs += ['src/notFatweb/java'] + } + + arm64 { + java.srcDirs += ['src/notFatweb/java'] + } + } /** @@ -241,6 +291,8 @@ android { * -------------------- * customProductionFatDebug -> branded development build, includes all ABIs * normalProductionFatDebug -> Bitmask development build, includes all ABIS + * customProductionFatwebDebug -> branded development build, includes all ABIs, for distribution through a download page + * normalProductionFatWebDebug -> Bitmask development build, includes all ABIS, for distribution through a download page * customInsecureFatDebug -> branded development build, doesn't checks certificates (for test server setup w/o valid certificates), includes all ABIs * normalInsecureFatDebug -> Bitmask development build, doesn't checks certificates (for test server setup w/o valid certificates), includes all ABIs * @@ -248,6 +300,7 @@ android { * ----------------- * customProductionFatBeta -> branded build, includes all ABI's, Beta release * customProductionFatRelease -> branded build, includes all ABI's, stable release (-> F-Droid, GPlay if not splitApk is set to true) + * customProductionFatwebRelease -> branded build, includes all ABI's, stable release (-> F-Droid, GPlay if not splitApk is set to true), for distribution through a download page * * Bitmask Beta releases: * ---------------------- @@ -263,7 +316,8 @@ android { * normalProductionArmv7Release -> Bitmask build, only for ABI armeabi-v7a, for GPlay releases with split apks (2 of 4) * normalProductionX86Release -> Bitmask build, only for ABI x86, for GPlay releases with split apks (3 of 4) * normalProductionX86_64Release -> Bitmask build, only for ABI x86 64 bit, for GPlay releases with split apks (4 of 4) - * normalProductionFatRelease -> Bitmask build, including all ABIS, for official F-Droid repo and stable link on download page + * normalProductionFatRelease -> Bitmask build, including all ABIS, for official F-Droid repo + * normalProductionFatWebRelease -> Bitmask build, including all ABIS, for distribution through a download page */ variantFilter { variant -> @@ -276,7 +330,8 @@ android { if (((names.contains("insecure") && !names.contains("fat")) || (names.contains("insecure") && buildTypeName.contains("beta")) || (names.contains("insecure") && buildTypeName.contains("release")) || - (buildTypeName.contains("debug") && !names.contains("fat")) || + (buildTypeName.contains("debug") && !(names.contains("fatweb") || names.contains("fat"))) || + (names.contains("fatweb") && buildTypeName.contains("beta")) || (!supportsSplitApk && !names.contains("fat"))) ) { // Gradle ignores any variants that satisfy the conditions above. @@ -330,7 +385,13 @@ dependencies { implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' implementation 'de.hdodenhof:circleimageview:3.1.0' - implementation project(path: ':shapeshifter') + //implementation project(path: ':shapeshifter') + fatwebImplementation project(path: ':bitmask-web-core') + fatImplementation project(path: ':bitmask-core') + x86Implementation project(path: ':bitmask-core') + x86_64Implementation project(path: ':bitmask-core') + armv7Implementation project(path: ':bitmask-core') + arm64Implementation project(path: ':bitmask-core') } android.applicationVariants.all { variant -> diff --git a/app/src/fatweb/AndroidManifest.xml b/app/src/fatweb/AndroidManifest.xml new file mode 100644 index 00000000..5188b05b --- /dev/null +++ b/app/src/fatweb/AndroidManifest.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/fatweb/assets/public.pgp b/app/src/fatweb/assets/public.pgp new file mode 100644 index 00000000..6964df42 --- /dev/null +++ b/app/src/fatweb/assets/public.pgp @@ -0,0 +1,458 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=9gOD +-----END PGP PUBLIC KEY BLOCK----- diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java new file mode 100644 index 00000000..6613d394 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadBroadcastReceiver.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import java.io.File; + +import se.leap.bitmaskclient.Constants; + +import static android.app.Activity.RESULT_CANCELED; +import static se.leap.bitmaskclient.Constants.BROADCAST_DOWNLOAD_SERVICE_EVENT; +import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; +import static se.leap.bitmaskclient.appUpdate.DownloadConnector.APP_TYPE; +import static se.leap.bitmaskclient.appUpdate.DownloadService.DOWNLOAD_FAILED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.DOWNLOAD_PROGRESS; +import static se.leap.bitmaskclient.appUpdate.DownloadService.NO_NEW_VERISON; +import static se.leap.bitmaskclient.appUpdate.DownloadService.PROGRESS_VALUE; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_DOWNLOADED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_DOWNLOAD_FAILED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_FOUND; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_NOT_FOUND; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.DOWNLOAD_UPDATE; +import static se.leap.bitmaskclient.appUpdate.FileProviderUtil.getUriFor; + +public class DownloadBroadcastReceiver extends BroadcastReceiver { + + public static final String ACTION_DOWNLOAD = "se.leap.bitmaskclient.appUpdate.ACTION_DOWNLOAD"; + private static final String TAG = DownloadBroadcastReceiver.class.getSimpleName(); + + private DownloadNotificationManager notificationManager; + + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "DOWNLOAD ON RECEIVE!"); + String action = intent.getAction(); + if (action == null) { + return; + } + + if (notificationManager == null) { + notificationManager = new DownloadNotificationManager(context.getApplicationContext()); + } + + int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); + Bundle resultData = intent.getParcelableExtra(Constants.BROADCAST_RESULT_KEY); + + switch (action) { + case BROADCAST_DOWNLOAD_SERVICE_EVENT: + switch (resultCode) { + case UPDATE_FOUND: + notificationManager.buildDownloadFoundNotification(); + break; + case UPDATE_NOT_FOUND: + if (resultData.getBoolean(NO_NEW_VERISON, false)) { + //TODO: Save in preferences date, retry in a week + } else if (resultData.getBoolean(DOWNLOAD_FAILED, false)) { + Toast.makeText(context.getApplicationContext(), "Update check failed.", Toast.LENGTH_LONG).show(); + } + break; + case UPDATE_DOWNLOADED: + notificationManager.cancelNotifications(); + Intent installIntent = new Intent(Intent.ACTION_VIEW); + File update = UpdateDownloadManager.getUpdateFile(context); + if (update.exists()) { + installIntent.setDataAndType(getUriFor(context, update), APP_TYPE); + } + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + context.startActivity(installIntent); + break; + case UPDATE_DOWNLOAD_FAILED: + notificationManager.cancelNotifications(); + Toast.makeText(context.getApplicationContext(), "Update download failed.", Toast.LENGTH_LONG).show(); + break; + case DOWNLOAD_PROGRESS: + int progress = resultData.getInt(PROGRESS_VALUE, 0); + notificationManager.buildDownloadUpdateProgress(progress); + break; + } + break; + + case ACTION_DOWNLOAD: + DownloadServiceCommand.execute(context.getApplicationContext(), DOWNLOAD_UPDATE); + break; + + default: + break; + } + + } +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java new file mode 100644 index 00000000..9427083d --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadConnector.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ +package se.leap.bitmaskclient.appUpdate; + + +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.io.InputStream; +import java.util.List; +import java.util.Scanner; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSink; +import okio.BufferedSource; +import okio.Okio; + + +/** + * This class encapsulates HTTP requests so that the results can be mocked + * and it's owning UpdateDownloadManager class logic can be unit tested properly + * + */ +public class DownloadConnector { + + private static final String TAG = DownloadConnector.class.getSimpleName(); + final static String APP_TYPE = "application/vnd.android.package-archive"; + final static String TEXT_FILE_TYPE = "application/text"; + + public interface DownloadProgress { + void onUpdate(int progress); + } + + static String requestTextFileFromServer(@NonNull String url, @NonNull OkHttpClient okHttpClient) { + try { + Request request = new Request.Builder() + .url(url) + .addHeader("Content-Type", TEXT_FILE_TYPE) + .build(); + + Response response = okHttpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + return null; + } + InputStream inputStream = response.body().byteStream(); + Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); + if (scanner.hasNext()) { + return scanner.next(); + } + return null; + + } catch (Exception e) { + Log.d(TAG, "Text file download failed"); + } + + return null; + } + + static File requestFileFromServer(@NonNull String url, @NonNull OkHttpClient okHttpClient, File destFile, DownloadProgress callback) { + BufferedSink sink = null; + BufferedSource source = null; + try { + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .addHeader("Content-Type", APP_TYPE); + Request request = requestBuilder.build(); + + Response response = okHttpClient.newCall(request).execute(); + ResponseBody body = response.body(); + InputStream in = body.byteStream(); + long contentLength = body.contentLength(); + source = body.source(); + sink = Okio.buffer(Okio.sink(destFile)); + Buffer sinkBuffer = sink.buffer(); + long totalBytesRead = 0; + int bufferSize = 8 * 1024; + long bytesRead; + while ((bytesRead = source.read(sinkBuffer, bufferSize)) != -1) { + sink.emit(); + totalBytesRead += bytesRead; + int progress = (int) ((totalBytesRead * 100) / contentLength); + callback.onUpdate(progress); + } + sink.flush(); + + return destFile; + + } catch (Exception e) { + Log.d(TAG, "File download failed"); + } + + return null; + } + +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java new file mode 100644 index 00000000..4f7f2883 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadNotificationManager.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; + +import se.leap.bitmaskclient.R; + +import static android.content.Intent.CATEGORY_DEFAULT; +import static se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver.ACTION_DOWNLOAD; + +public class DownloadNotificationManager { + private Context context; + private final static int DOWNLOAD_NOTIFICATION_ID = 1; + + public DownloadNotificationManager(@NonNull Context context) { + this.context = context; + } + + public void buildDownloadFoundNotification() { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(notificationManager); + } + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this.context, DownloadService.NOTIFICATION_CHANNEL_NEWSTATUS_ID); + notificationBuilder.setAutoCancel(true) + .setDefaults(Notification.DEFAULT_ALL) + .setWhen(System.currentTimeMillis()) + .setSmallIcon(R.mipmap.ic_launcher) + .setTicker(context.getString(R.string.version_update_title, context.getString(R.string.app_name))) + .setContentTitle(context.getString(R.string.version_update_title, context.getString(R.string.app_name))) + .setContentText(context.getString(R.string.version_update_found)) + .setContentIntent(getDownloadIntent()); + notificationManager.notify(DOWNLOAD_NOTIFICATION_ID, notificationBuilder.build()); + } + + @TargetApi(26) + private void createNotificationChannel(NotificationManager notificationManager) { + CharSequence name = "Bitmask Updates"; + String description = "Informs about available updates"; + NotificationChannel channel = new NotificationChannel(DownloadService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, + name, + NotificationManager.IMPORTANCE_LOW); + channel.setSound(null, null); + channel.setDescription(description); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + notificationManager.createNotificationChannel(channel); + } + + + + private PendingIntent getDownloadIntent() { + Intent downloadIntent = new Intent(context, DownloadBroadcastReceiver.class); + downloadIntent.setAction(ACTION_DOWNLOAD); + return PendingIntent.getBroadcast(context, 0, downloadIntent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + public void buildDownloadUpdateProgress(int progress) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(notificationManager); + } + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this.context, DownloadService.NOTIFICATION_CHANNEL_NEWSTATUS_ID); + notificationBuilder.setAutoCancel(true) + .setDefaults(Notification.DEFAULT_ALL) + .setAutoCancel(false) + .setOngoing(true) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(context.getString(R.string.version_update_apk_description, context.getString(R.string.app_name))) + .setProgress(100, progress, false) + .setContentIntent(getDownloadIntent()); + notificationManager.notify(DOWNLOAD_NOTIFICATION_ID, notificationBuilder.build()); + } + + public void cancelNotifications() { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + return; + } + notificationManager.cancel(DOWNLOAD_NOTIFICATION_ID); + } +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadService.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadService.java new file mode 100644 index 00000000..bc9adfc1 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadService.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.core.app.JobIntentService; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import se.leap.bitmaskclient.OkHttpClientGenerator; + +public class DownloadService extends JobIntentService implements UpdateDownloadManager.DownloadServiceCallback { + + static final int JOB_ID = 161376; + static final String NOTIFICATION_CHANNEL_NEWSTATUS_ID = "bitmask_download_service_news"; + + final public static String TAG = DownloadService.class.getSimpleName(), + PROGRESS_VALUE = "progressValue", + NO_NEW_VERISON = "noNewVersion", + DOWNLOAD_FAILED = "downloadFailed", + NO_PUB_KEY = "noPubKey", + VERIFICATION_ERROR = "verificationError"; + + final public static int + UPDATE_DOWNLOADED = 1, + UPDATE_DOWNLOAD_FAILED = 2, + UPDATE_FOUND = 3, + UPDATE_NOT_FOUND = 4, + DOWNLOAD_PROGRESS = 6; + + + private UpdateDownloadManager updateDownloadManager; + + + @Override + public void onCreate() { + super.onCreate(); + updateDownloadManager = initDownloadManager(); + } + + @Override + protected void onHandleWork(@NonNull Intent intent) { + updateDownloadManager.handleIntent(intent); + } + + /** + * Convenience method for enqueuing work in to this service. + */ + static void enqueueWork(Context context, Intent work) { + try { + DownloadService.enqueueWork(context, DownloadService.class, JOB_ID, work); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + } + + private UpdateDownloadManager initDownloadManager() { + OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(null); + return new UpdateDownloadManager(this, clientGenerator, this); + } + + @Override + public void broadcastEvent(Intent intent) { + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadServiceCommand.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadServiceCommand.java new file mode 100644 index 00000000..c4e809f2 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/DownloadServiceCommand.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.content.Context; +import android.content.Intent; +import android.os.ResultReceiver; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import se.leap.bitmaskclient.ProviderAPI; + +public class DownloadServiceCommand { + + public final static String + CHECK_VERSION_FILE = "checkVersionFile", + DOWNLOAD_UPDATE = "downloadUpdate"; + + private Context context; + private String action; + private ResultReceiver resultReceiver; + + private DownloadServiceCommand(@NotNull Context context, @NotNull String action) { + this(context.getApplicationContext(), action, null); + } + + private DownloadServiceCommand(@NotNull Context context, @NotNull String action, @Nullable ResultReceiver resultReceiver) { + super(); + this.context = context; + this.action = action; + this.resultReceiver = resultReceiver; + } + + + private Intent setUpIntent() { + Intent command = new Intent(context, ProviderAPI.class); + command.setAction(action); + if (resultReceiver != null) { + command.putExtra(ProviderAPI.RECEIVER_KEY, resultReceiver); + } + return command; + } + + private boolean isInitialized() { + return context != null; + } + + + private void execute() { + if (isInitialized()) { + Intent intent = setUpIntent(); + DownloadService.enqueueWork(context, intent); + } + } + + public static void execute(Context context, String action) { + DownloadServiceCommand command = new DownloadServiceCommand(context, action); + command.execute(); + } + + public static void execute(Context context, String action, ResultReceiver resultReceiver) { + DownloadServiceCommand command = new DownloadServiceCommand(context, action, resultReceiver); + command.execute(); + } + +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/FileProviderUtil.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/FileProviderUtil.java new file mode 100644 index 00000000..756a3b99 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/FileProviderUtil.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; + +import java.io.File; + +import se.leap.bitmaskclient.BuildConfig; + +/** + * From Signal + */ + +public class FileProviderUtil { + + private static final String AUTHORITY = BuildConfig.APPLICATION_ID +".fileprovider"; + + public static Uri getUriFor(@NonNull Context context, @NonNull File file) { + if (Build.VERSION.SDK_INT >= 24) return FileProvider.getUriForFile(context, AUTHORITY, file); + else return Uri.fromFile(file); + } + + public static boolean isAuthority(@NonNull Uri uri) { + return AUTHORITY.equals(uri.getAuthority()); + } + + public static boolean delete(@NonNull Context context, @NonNull Uri uri) { + if (AUTHORITY.equals(uri.getAuthority())) { + return context.getContentResolver().delete(uri, null, null) > 0; + } + return new File(uri.getPath()).delete(); + } +} diff --git a/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java new file mode 100644 index 00000000..698a0d17 --- /dev/null +++ b/app/src/fatweb/java/se.leap.bitmaskclient/appUpdate/UpdateDownloadManager.java @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ +package se.leap.bitmaskclient.appUpdate; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.File; + +import okhttp3.OkHttpClient; +import pgpverify.Logger; +import pgpverify.PgpVerifier; +import se.leap.bitmaskclient.BuildConfig; +import se.leap.bitmaskclient.OkHttpClientGenerator; +import se.leap.bitmaskclient.R; + +import static android.text.TextUtils.isEmpty; +import static se.leap.bitmaskclient.Constants.BROADCAST_DOWNLOAD_SERVICE_EVENT; +import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; +import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.ProviderAPI.RECEIVER_KEY; +import static se.leap.bitmaskclient.appUpdate.DownloadService.DOWNLOAD_FAILED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.DOWNLOAD_PROGRESS; +import static se.leap.bitmaskclient.appUpdate.DownloadService.NO_NEW_VERISON; +import static se.leap.bitmaskclient.appUpdate.DownloadService.NO_PUB_KEY; +import static se.leap.bitmaskclient.appUpdate.DownloadService.PROGRESS_VALUE; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_DOWNLOADED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_DOWNLOAD_FAILED; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_FOUND; +import static se.leap.bitmaskclient.appUpdate.DownloadService.UPDATE_NOT_FOUND; +import static se.leap.bitmaskclient.appUpdate.DownloadService.VERIFICATION_ERROR; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.DOWNLOAD_UPDATE; +import static se.leap.bitmaskclient.utils.FileHelper.readPublicKey; + +public class UpdateDownloadManager implements Logger, DownloadConnector.DownloadProgress { + + + private static final String TAG = UpdateDownloadManager.class.getSimpleName(); + + public interface DownloadServiceCallback { + void broadcastEvent(Intent intent); + } + + private Context context; + + private PgpVerifier pgpVerifier; + private DownloadServiceCallback serviceCallback; + OkHttpClientGenerator clientGenerator; + + + public UpdateDownloadManager(Context context, OkHttpClientGenerator clientGenerator, DownloadServiceCallback callback) { + this.context = context; + this.clientGenerator = clientGenerator; + pgpVerifier = new PgpVerifier(); + pgpVerifier.setLogger(this); + serviceCallback = callback; + } + + //pgpverify Logger interface + @Override + public void log(String s) { + + } + + @Override + public void onUpdate(int progress) { + Bundle resultData = new Bundle(); + resultData.putInt(PROGRESS_VALUE, progress); + broadcastEvent(DOWNLOAD_PROGRESS, resultData); + } + + public void handleIntent(Intent command) { + ResultReceiver receiver = null; + if (command.getParcelableExtra(RECEIVER_KEY) != null) { + receiver = command.getParcelableExtra(RECEIVER_KEY); + } + String action = command.getAction(); + + Bundle result = new Bundle(); + switch (action) { + case CHECK_VERSION_FILE: + result = checkVersionFile(result); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + sendToReceiverOrBroadcast(receiver, UPDATE_FOUND, result); + } else { + sendToReceiverOrBroadcast(receiver, UPDATE_NOT_FOUND, result); + } + break; + case DOWNLOAD_UPDATE: + result = downloadUpdate(result); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + sendToReceiverOrBroadcast(receiver, UPDATE_DOWNLOADED, result); + } else { + sendToReceiverOrBroadcast(receiver, UPDATE_DOWNLOAD_FAILED, result); + } + break; + + } + } + + public static File getUpdateFile(Context context) { + return new File(context.getExternalFilesDir(null) + "/" + context.getString(R.string.app_name) + "_update.apk"); + } + + private Bundle downloadUpdate(Bundle task) { + + String publicKey = readPublicKey(context); + if (isEmpty(publicKey)) { + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(NO_PUB_KEY, true); + return task; + } + + OkHttpClient client = clientGenerator.init(); + String signature = DownloadConnector.requestTextFileFromServer(BuildConfig.signature_url, client); + if (signature == null) { + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(DOWNLOAD_FAILED, true); + return task; + } + + File destinationFile = getUpdateFile(context); + if (destinationFile.exists()) { + destinationFile.delete(); + } + + destinationFile = DownloadConnector.requestFileFromServer(BuildConfig.update_apk_url, client, destinationFile, this); + + if (destinationFile == null) { + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(DOWNLOAD_FAILED, true); + return task; + } + + boolean successfulVerified = pgpVerifier.verify(signature, publicKey, destinationFile.getAbsolutePath()); + if (!successfulVerified) { + destinationFile.delete(); + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(VERIFICATION_ERROR, true); + return task; + } + + task.putBoolean(BROADCAST_RESULT_KEY, true); + return task; + } + + private static void clearPreviousDownloads(@NonNull Context context, String destinationFile) { + File directory = context.getExternalFilesDir(null); + + if (directory == null) { + Log.w(TAG, "Failed to read external files directory."); + return; + } + + for (File file : directory.listFiles()) { + if (file.getName().equals(destinationFile)) { + if (file.delete()) { + Log.d(TAG, "Deleted " + file.getName()); + } + } + } + } + + private Bundle checkVersionFile(Bundle task) { + OkHttpClient client = clientGenerator.init(); + String versionString = DownloadConnector.requestTextFileFromServer(BuildConfig.version_file_url, client); + + if (versionString != null) { + versionString = versionString.replace("\n", "").trim(); + } + + int version = -1; + try { + version = Integer.valueOf(versionString); + } catch (NumberFormatException e) { + e.printStackTrace(); + Log.e(TAG, "could not parse version code: " + versionString); + } + + if (version == -1) { + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(DOWNLOAD_FAILED, true); + } else if (BuildConfig.VERSION_CODE >= version) { + task.putBoolean(BROADCAST_RESULT_KEY, false); + task.putBoolean(NO_NEW_VERISON, true); + } else { + task.putBoolean(BROADCAST_RESULT_KEY, true); + } + return task; + } + + private void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData) { + if (resultData == null || resultData == Bundle.EMPTY) { + resultData = new Bundle(); + } + if (receiver != null) { + receiver.send(resultCode, resultData); + } else { + broadcastEvent(resultCode, resultData); + } + } + + private void broadcastEvent(int resultCode , Bundle resultData) { + Intent intentUpdate = new Intent(BROADCAST_DOWNLOAD_SERVICE_EVENT); + intentUpdate.addCategory(Intent.CATEGORY_DEFAULT); + intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode); + intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData); + serviceCallback.broadcastEvent(intentUpdate); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java index bde5114b..437998e0 100644 --- a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java +++ b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java @@ -1,16 +1,42 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ + package se.leap.bitmaskclient; import android.content.Context; +import android.content.IntentFilter; import android.content.SharedPreferences; -import androidx.multidex.MultiDexApplication; + import androidx.appcompat.app.AppCompatDelegate; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.multidex.MultiDexApplication; import com.squareup.leakcanary.LeakCanary; import com.squareup.leakcanary.RefWatcher; +import se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver; import se.leap.bitmaskclient.tethering.TetheringStateManager; +import static android.content.Intent.CATEGORY_DEFAULT; +import static se.leap.bitmaskclient.Constants.BROADCAST_DOWNLOAD_SERVICE_EVENT; import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver.ACTION_DOWNLOAD; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.DOWNLOAD_UPDATE; import static se.leap.bitmaskclient.utils.PreferenceHelper.getSavedProviderFromSharedPreferences; /** @@ -22,6 +48,7 @@ public class BitmaskApp extends MultiDexApplication { private final static String TAG = BitmaskApp.class.getSimpleName(); private RefWatcher refWatcher; private ProviderObservable providerObservable; + private DownloadBroadcastReceiver downloadBroadcastReceiver; @Override @@ -41,6 +68,15 @@ public class BitmaskApp extends MultiDexApplication { EipSetupObserver.init(this, preferences); AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); TetheringStateManager.getInstance().init(this); + if (BuildConfig.FLAVOR.contains("Fatweb")) { + downloadBroadcastReceiver = new DownloadBroadcastReceiver(); + IntentFilter intentFilter = new IntentFilter(BROADCAST_DOWNLOAD_SERVICE_EVENT); + intentFilter.addAction(ACTION_DOWNLOAD); + intentFilter.addAction(CHECK_VERSION_FILE); + intentFilter.addAction(DOWNLOAD_UPDATE); + intentFilter.addCategory(CATEGORY_DEFAULT); + LocalBroadcastManager.getInstance(this.getApplicationContext()).registerReceiver(downloadBroadcastReceiver, intentFilter); + } } /** diff --git a/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java b/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java index 0ef77e2b..4f27f88a 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java +++ b/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java @@ -1,3 +1,19 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ package se.leap.bitmaskclient; import androidx.appcompat.app.AppCompatActivity; diff --git a/app/src/main/java/se/leap/bitmaskclient/Constants.java b/app/src/main/java/se/leap/bitmaskclient/Constants.java index 6462b663..dfd94759 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Constants.java +++ b/app/src/main/java/se/leap/bitmaskclient/Constants.java @@ -1,3 +1,19 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ package se.leap.bitmaskclient; import android.text.TextUtils; @@ -20,6 +36,9 @@ public interface Constants { String ALLOW_TETHERING_USB = "tethering_usb"; String SHOW_EXPERIMENTAL = "show_experimental"; String USE_IPv6_FIREWALL = "use_ipv6_firewall"; + String APK_DOWNLOAD_ID = "apk_download_id"; + String VERSION_FILE_DOWNLOAD_ID = "version_file_downlaod_id"; + String SIGNATURE_DOWNLOAD_ID = "signature_file_download_id"; ////////////////////////////////////////////// @@ -105,6 +124,7 @@ public interface Constants { String BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT = "BROADCAST.GATEWAY_SETUP_WATCHER_EVENT"; String BROADCAST_RESULT_CODE = "BROADCAST.RESULT_CODE"; String BROADCAST_RESULT_KEY = "BROADCAST.RESULT_KEY"; + String BROADCAST_DOWNLOAD_SERVICE_EVENT = "BROADCAST.DOWNLOAD_SERVICE_EVENT"; ////////////////////////////////////////////// diff --git a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java index 7504e0c0..174e6657 100644 --- a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java +++ b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java @@ -1,3 +1,20 @@ +/** + * Copyright (c) 2020 LEAP Encryption Access Project and contributers + * + * This program 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 3 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, see . + */ + package se.leap.bitmaskclient; import android.content.BroadcastReceiver; @@ -21,6 +38,7 @@ import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.ConnectionStatus; import de.blinkt.openvpn.core.LogItem; import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.appUpdate.DownloadServiceCommand; import se.leap.bitmaskclient.eip.EIP; import se.leap.bitmaskclient.eip.EipCommand; import se.leap.bitmaskclient.eip.EipStatus; @@ -48,6 +66,7 @@ import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON; import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE; /** * Created by cyberta on 05.12.18. @@ -292,6 +311,8 @@ class EipSetupObserver extends BroadcastReceiver implements VpnStatus.StateListe //setupNClostestGateway > 0: at least one failed gateway -> did the provider change it's gateways? ProviderAPICommand.execute(context, ProviderAPI.DOWNLOAD_SERVICE_JSON, provider); } + + DownloadServiceCommand.execute(context, CHECK_VERSION_FILE); finishGatewaySetup(false); } else if ("TCP_CONNECT".equals(state)) { changingGateway.set(false); diff --git a/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java b/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java index 7b9874e0..576e76e0 100644 --- a/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java +++ b/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java @@ -62,7 +62,7 @@ public class OkHttpClientGenerator { Resources resources; - public OkHttpClientGenerator(SharedPreferences preferences, Resources resources) { + public OkHttpClientGenerator(/*SharedPreferences preferences,*/ Resources resources) { this.resources = resources; } @@ -74,22 +74,21 @@ public class OkHttpClientGenerator { return initHttpClient(initError, caCert); } + public OkHttpClient init() { + try { + return createClient(null); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + private OkHttpClient initHttpClient(JSONObject initError, String certificate) { + if (resources == null) { + return null; + } try { - TLSCompatSocketFactory sslCompatFactory; - ConnectionSpec spec = getConnectionSpec(); - OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); - - if (!isEmpty(certificate)) { - sslCompatFactory = new TLSCompatSocketFactory(certificate); - } else { - sslCompatFactory = new TLSCompatSocketFactory(); - } - sslCompatFactory.initSSLSocketFactory(clientBuilder); - clientBuilder.cookieJar(getCookieJar()) - .connectionSpecs(Collections.singletonList(spec)); - clientBuilder.dns(new DnsResolver()); - return clientBuilder.build(); + return createClient(certificate); } catch (IllegalArgumentException e) { e.printStackTrace(); // TODO ca cert is invalid - show better error ?! @@ -110,10 +109,31 @@ public class OkHttpClientGenerator { } catch (IOException e) { e.printStackTrace(); addErrorMessageToJson(initError, resources.getString(error_io_exception_user_message)); + } catch (Exception e) { + e.printStackTrace(); + // unexpected exception, should never happen + // only to shorten the method signature createClient(String certificate) } return null; } + private OkHttpClient createClient(String certificate) throws Exception { + TLSCompatSocketFactory sslCompatFactory; + ConnectionSpec spec = getConnectionSpec(); + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + + if (!isEmpty(certificate)) { + sslCompatFactory = new TLSCompatSocketFactory(certificate); + } else { + sslCompatFactory = new TLSCompatSocketFactory(); + } + sslCompatFactory.initSSLSocketFactory(clientBuilder); + clientBuilder.cookieJar(getCookieJar()) + .connectionSpecs(Collections.singletonList(spec)); + clientBuilder.dns(new DnsResolver()); + return clientBuilder.build(); + } + @NonNull diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java index 4058b824..bec16139 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java @@ -120,7 +120,7 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB private ProviderApiManager initApiManager() { SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(preferences, getResources()); + OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(getResources()); return new ProviderApiManager(preferences, getResources(), clientGenerator, this); } } diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java index 2748c944..5a142d90 100644 --- a/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java +++ b/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java @@ -53,6 +53,7 @@ import static se.leap.bitmaskclient.Constants.DEFAULT_BITMASK; /** * Stores constants, and implements auxiliary methods used across all Bitmask Android classes. + * Wraps BuildConfigFields for to support easier unit testing * * @author parmegv * @author MeanderingCode @@ -120,7 +121,7 @@ public class ConfigHelper { try { KeyFactory kf; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - kf = KeyFactory.getInstance("RSA", "BC"); + kf = KeyFactory.getInstance("RSA", "BC"); } else { kf = KeyFactory.getInstance("RSA"); } @@ -201,4 +202,29 @@ public class ConfigHelper { return (string1 == null && string2 == null) || (string1 != null && string1.equals(string2)); } + + public static String getApkFileName() { + try { + return BuildConfig.update_apk_url.substring(BuildConfig.update_apk_url.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + + public static String getVersionFileName() { + try { + return BuildConfig.version_file_url.substring(BuildConfig.version_file_url.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + + public static String getSignatureFileName() { + try { + return BuildConfig.signature_url.substring(BuildConfig.signature_url.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + } diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java index 1c3e1ebb..ebcc32ba 100644 --- a/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java +++ b/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java @@ -1,8 +1,13 @@ package se.leap.bitmaskclient.utils; +import android.content.Context; + +import java.io.BufferedReader; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; /** * Created by cyberta on 18.03.18. @@ -19,4 +24,23 @@ public class FileHelper { writer.close(); } + public static String readPublicKey(Context context) { + { + InputStream inputStream; + try { + inputStream = context.getAssets().open("public.pgp"); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } catch (IOException errabi) { + return null; + } + } + } + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6d4c62e1..dd1ddeb4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,4 +142,10 @@ %s cannot execute features like VPN Hotspot or IPv6 firewall without root permissions. Start %s + Tap here to start the download. + A new %s version has been found. + Downloading a new %s version + Storage access is required to web update. + Storage permission request was denied. + diff --git a/app/src/main/res/xml/file_provider_paths.xml b/app/src/main/res/xml/file_provider_paths.xml new file mode 100644 index 00000000..5b9dd9fb --- /dev/null +++ b/app/src/main/res/xml/file_provider_paths.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadBroadcastReceiver.java b/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadBroadcastReceiver.java new file mode 100644 index 00000000..7adbb85d --- /dev/null +++ b/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadBroadcastReceiver.java @@ -0,0 +1,18 @@ +package se.leap.bitmaskclient.appUpdate; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * DownloadBroadcastReceiver is only implemented in Fatweb builds + * + */ +public class DownloadBroadcastReceiver extends BroadcastReceiver { + + public static final String ACTION_DOWNLOAD = "se.leap.bitmaskclient.appUpdate.ACTION_DOWNLOAD"; + @Override + public void onReceive(Context context, Intent intent) { + + } +} diff --git a/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadServiceCommand.java b/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadServiceCommand.java new file mode 100644 index 00000000..157f04fe --- /dev/null +++ b/app/src/notFatweb/java/se/leap/bitmaskclient/appUpdate/DownloadServiceCommand.java @@ -0,0 +1,19 @@ +package se.leap.bitmaskclient.appUpdate; + +import android.content.Context; + +/** + * DownloadServiceCommand is only implemented in Fatweb builds + * + */ +public class DownloadServiceCommand { + + public final static String + CHECK_VERSION_FILE = "checkVersionFile", + DOWNLOAD_UPDATE = "downloadUpdate"; + + + public static void execute(Context context, String action) { + // DO NOTHING. + } +} diff --git a/bitmask-core/bitmask-core-sources.jar b/bitmask-core/bitmask-core-sources.jar new file mode 100644 index 00000000..adf7f860 Binary files /dev/null and b/bitmask-core/bitmask-core-sources.jar differ diff --git a/bitmask-core/bitmask-core.aar b/bitmask-core/bitmask-core.aar new file mode 100644 index 00000000..37e25808 Binary files /dev/null and b/bitmask-core/bitmask-core.aar differ diff --git a/bitmask-core/build.gradle b/bitmask-core/build.gradle new file mode 100644 index 00000000..51d38614 --- /dev/null +++ b/bitmask-core/build.gradle @@ -0,0 +1,2 @@ +configurations.maybeCreate("default") +artifacts.add("default", file('bitmask-core.aar')) \ No newline at end of file diff --git a/bitmask-web-core/bitmask-web-core-sources.jar b/bitmask-web-core/bitmask-web-core-sources.jar new file mode 100644 index 00000000..6788dc00 Binary files /dev/null and b/bitmask-web-core/bitmask-web-core-sources.jar differ diff --git a/bitmask-web-core/bitmask-web-core.aar b/bitmask-web-core/bitmask-web-core.aar new file mode 100644 index 00000000..6f580439 Binary files /dev/null and b/bitmask-web-core/bitmask-web-core.aar differ diff --git a/bitmask-web-core/build.gradle b/bitmask-web-core/build.gradle new file mode 100644 index 00000000..650f38d6 --- /dev/null +++ b/bitmask-web-core/build.gradle @@ -0,0 +1,37 @@ +/*apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + buildToolsVersion "29.0.1" + + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'androidx.appcompat:appcompat:1.2.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} +*/ +configurations.maybeCreate("default") +artifacts.add("default", file('bitmask-web-core.aar')) \ No newline at end of file diff --git a/build_deps.sh b/build_deps.sh index 156f5300..beb5e13e 100755 --- a/build_deps.sh +++ b/build_deps.sh @@ -19,7 +19,7 @@ if [[ $(ls -A ${DIR_OVPNASSETS}) && $(ls -A ${DIR_OVPNLIBS}) ]] then echo "Dirty build: skipped externalNativeBuild - reusing existing libs" else - echo "Clean build: starting externalNativeBuild and GO dependency builds" + echo "Clean build: starting externalNativeBuild" cd ./ics-openvpn || quit "Directory ics-opevpn not found" ./gradlew clean main:externalNativeBuildCleanSkeletonRelease main:externalNativeBuildSkeletonRelease --debug --stacktrace || quit "Build ics-openvpn native libraries failed" cd .. @@ -32,6 +32,7 @@ else echo "Clean build: compiling Go libraries" cd ./go || quit "Directory go not found" ./install_go.sh || quit "install_go.sh failed" - ./android_build_shapeshifter.sh createLibrary || quit "android_build_shapeshifter_dispatcher.sh failed" + ./android_build_web_core.sh || quit "android_build_web_core.sh (shapeshifter + pgpverify) failed" + ./android_build_core.sh || quit "android build core (shapeshifter) failed" cd .. fi diff --git a/go/android_build_core.sh b/go/android_build_core.sh new file mode 100755 index 00000000..1a6af5b7 --- /dev/null +++ b/go/android_build_core.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +export GOPATH=`pwd` +export GO_LANG=`pwd`/golang/go/bin +export GO_COMPILED=`pwd`/bin +PATH="${GO_LANG}:${GO_COMPILED}:${PATH}" + +if [ -z $ANDROID_NDK_HOME ]; then + echo "Android NDK path not specified!" + echo "Please set \$ANDROID_NDK_HOME before starting this script!" + exit 1; +fi + +./golang/go/bin/go env +echo "getting gomobile..." +./golang/go/bin/go get golang.org/x/mobile/cmd/gomobile +echo "initiating gomobile..." +./bin/gomobile init +if [ ! -d ./lib ]; then mkdir ./lib; fi +echo "cross compiling bitmask core lib (shapeshifter)..." +./bin/gomobile bind -target=android -o ./lib/bitmask-core.aar se.leap.bitmaskclient/shapeshifter/ +cp lib/bitmask-core* ../bitmask-core/. \ No newline at end of file diff --git a/go/android_build_shapeshifter_lib.sh b/go/android_build_shapeshifter_lib.sh deleted file mode 100755 index bbe3c84a..00000000 --- a/go/android_build_shapeshifter_lib.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -export GOPATH=`pwd` -export GO_LANG=`pwd`/golang/go/bin -export GO_COMPILED=`pwd`/bin -PATH="${GO_LANG}:${GO_COMPILED}:${PATH}" - -if [ -z $ANDROID_NDK_HOME ]; then - echo "Android NDK path not specified!" - echo "Please set \$ANDROID_NDK_HOME before starting this script!" - exit 1; -fi - -./golang/go/bin/go env -echo "getting gomobile..." -./golang/go/bin/go get golang.org/x/mobile/cmd/gomobile -echo "initiating gomobile..." -./bin/gomobile init -echo "cross compiling shapeshifter lib..." -./bin/gomobile bind -target=android -o ./lib/shapeshifter.aar se.leap.bitmaskclient/shapeshifter/ -cp lib/shapeshifter* ../shapeshifter/. \ No newline at end of file diff --git a/go/android_build_web_core.sh b/go/android_build_web_core.sh new file mode 100755 index 00000000..762d4dad --- /dev/null +++ b/go/android_build_web_core.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +export GOPATH=`pwd` +export GO_LANG=`pwd`/golang/go/bin +export GO_COMPILED=`pwd`/bin +PATH="${GO_LANG}:${GO_COMPILED}:${PATH}" + +if [ -z $ANDROID_NDK_HOME ]; then + echo "Android NDK path not specified!" + echo "Please set \$ANDROID_NDK_HOME before starting this script!" + exit 1; +fi + +./golang/go/bin/go env +echo "getting gomobile..." +./golang/go/bin/go get golang.org/x/mobile/cmd/gomobile +echo "initiating gomobile..." +./bin/gomobile init +if [ ! -d ./lib ]; then mkdir ./lib; fi +echo "cross compiling bitmask web apk core lib (shapeshifter, pgpverify)..." +./bin/gomobile bind -target=android -o ./lib/bitmask-web-core.aar se.leap.bitmaskclient/shapeshifter/ se.leap.bitmaskclient/pgpverify +cp lib/bitmask-web-core* ../bitmask-web-core/. \ No newline at end of file diff --git a/go/src/se.leap.bitmaskclient/pgpverify/pgpverify.go b/go/src/se.leap.bitmaskclient/pgpverify/pgpverify.go new file mode 100644 index 00000000..653ea695 --- /dev/null +++ b/go/src/se.leap.bitmaskclient/pgpverify/pgpverify.go @@ -0,0 +1,82 @@ +package pgpverify + +import ( + "os" + "strings" + + "golang.org/x/crypto/openpgp" +) + +// PgpVerifier - exported struct used for file verification +type PgpVerifier struct { + //Signature string + //Target string + //PublicKey string + Logger Logger +} + +// Logger - logging interface +type Logger interface { + Log(msg string) +} + +// Verify checks if a file was signed with the correct pgp key +// using a PEM formatted signature and a corresponding public key +func (pgpv *PgpVerifier) Verify(signature string, publicKey string, targetPath string) bool { + keyRingReader := strings.NewReader(publicKey) + signatureReader := strings.NewReader(signature) + + verificationTarget, err := os.Open(targetPath) + if err != nil { + pgpv.Logger.Log("Open verification target: " + err.Error()) + return false + } + + keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader) + if err != nil { + pgpv.Logger.Log("Read Armored Key Ring: " + err.Error()) + return false + } + _, err = openpgp.CheckArmoredDetachedSignature(keyring, verificationTarget, signatureReader) + if err != nil { + pgpv.Logger.Log("Verification failed: " + err.Error()) + return false + } + pgpv.Logger.Log("Successfully verified: entity.Identities") + return true +} + +/*func main() { + keyRingReader, err := os.Open("public_leap.asc") + if err != nil { + fmt.Println(err) + return + } + + signature, err := os.Open("RiseupVPN_release_1.0.5.apk.sig") + if err != nil { + fmt.Println(err) + return + } + + verificationTarget, err := os.Open("RiseupVPN_release_1.0.5.apk") + if err != nil { + fmt.Println(err) + return + } + + keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader) + if err != nil { + fmt.Println("Read Armored Key Ring: " + err.Error()) + return + } + entity, err := openpgp.CheckArmoredDetachedSignature(keyring, verificationTarget, signature) + if err != nil { + fmt.Println("Check Detached Signature: " + err.Error()) + return + } else { + fmt.Println("successfully verified") + } + + fmt.Println(entity.Identities) +}*/ diff --git a/settings.gradle b/settings.gradle index 4b8df4a4..08a7095e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':shapeshifter' \ No newline at end of file +include ':app', ':bitmask-core', ':bitmask-web-core' \ No newline at end of file diff --git a/shapeshifter/build.gradle b/shapeshifter/build.gradle deleted file mode 100644 index 667e13c9..00000000 --- a/shapeshifter/build.gradle +++ /dev/null @@ -1,2 +0,0 @@ -configurations.maybeCreate("default") -artifacts.add("default", file('shapeshifter.aar')) \ No newline at end of file diff --git a/shapeshifter/shapeshifter-sources.jar b/shapeshifter/shapeshifter-sources.jar deleted file mode 100644 index adf7f860..00000000 Binary files a/shapeshifter/shapeshifter-sources.jar and /dev/null differ diff --git a/shapeshifter/shapeshifter.aar b/shapeshifter/shapeshifter.aar deleted file mode 100644 index a9976239..00000000 Binary files a/shapeshifter/shapeshifter.aar and /dev/null differ -- cgit v1.2.3