From 76edcb900a851772fcbdce77afa6ad31c65b8d7e Mon Sep 17 00:00:00 2001 From: Jakob Nixdorf Date: Thu, 15 Mar 2018 11:20:44 +0100 Subject: [PATCH] Broadcast backup improvements * Add setting to enable backup types as discusses in #157 * Add different notification channels --- .../andotp/Activities/MainActivity.java | 2 + .../Receivers/BackupBroadcastReceiver.java | 11 +--- .../EncryptedBackupBroadcastReceiver.java | 63 ++++++++++--------- .../PlainTextBackupBroadcastReceiver.java | 47 +++++++------- .../flocke/andotp/Utilities/Constants.java | 4 ++ .../andotp/Utilities/NotificationHelper.java | 58 +++++++++++++---- .../flocke/andotp/Utilities/Settings.java | 12 ++++ app/src/main/res/values/settings.xml | 6 ++ app/src/main/res/values/strings_backup.xml | 14 ++++- app/src/main/res/values/strings_settings.xml | 9 +++ app/src/main/res/xml/preferences.xml | 8 +++ 11 files changed, 160 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java index be3b30f4..f1d33923 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java @@ -60,6 +60,7 @@ import org.shadowice.flocke.andotp.R; import org.shadowice.flocke.andotp.Utilities.Constants; import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; +import org.shadowice.flocke.andotp.Utilities.NotificationHelper; import org.shadowice.flocke.andotp.Utilities.TokenCalculator; import org.shadowice.flocke.andotp.View.EntriesCardAdapter; import org.shadowice.flocke.andotp.View.FloatingActionMenu; @@ -222,6 +223,7 @@ public class MainActivity extends BaseActivity ItemTouchHelper touchHelper = new ItemTouchHelper(touchHelperCallback); touchHelper.attachToRecyclerView(recList); + NotificationHelper.initializeNotificationChannels(this); restoreSortMode(); float durationScale = android.provider.Settings.Global.getFloat(this.getContentResolver(), android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 0); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Receivers/BackupBroadcastReceiver.java b/app/src/main/java/org/shadowice/flocke/andotp/Receivers/BackupBroadcastReceiver.java index 3c80b6e4..7a6af453 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Receivers/BackupBroadcastReceiver.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Receivers/BackupBroadcastReceiver.java @@ -24,33 +24,28 @@ package org.shadowice.flocke.andotp.Receivers; import android.Manifest; -import android.app.NotificationChannel; -import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.pm.PackageManager; -import android.os.Build; -import android.support.v4.app.NotificationCompat; import android.support.v4.content.ContextCompat; import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.Constants; import org.shadowice.flocke.andotp.Utilities.NotificationHelper; import org.shadowice.flocke.andotp.Utilities.Settings; import java.io.File; -import static android.content.Context.NOTIFICATION_SERVICE; - public abstract class BackupBroadcastReceiver extends BroadcastReceiver { protected boolean canSaveBackup(Context context) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_read_permission_failed); + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_read_permission_failed); return false; } if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_write_permission_failed); + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_write_permission_failed); return false; } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Receivers/EncryptedBackupBroadcastReceiver.java b/app/src/main/java/org/shadowice/flocke/andotp/Receivers/EncryptedBackupBroadcastReceiver.java index e6232dc8..253cea1e 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Receivers/EncryptedBackupBroadcastReceiver.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Receivers/EncryptedBackupBroadcastReceiver.java @@ -43,48 +43,53 @@ import java.util.ArrayList; import javax.crypto.SecretKey; +// Use the following command to test in the dev version: +// adb shell am broadcast -a org.shadowice.flocke.andotp.broadcast.ENCRYPTED_BACKUP org.shadowice.flocke.andotp.dev public class EncryptedBackupBroadcastReceiver extends BackupBroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if(!canSaveBackup(context)) - return; - Settings settings = new Settings(context); - Uri savePath = Tools.buildUri(settings.getBackupDir(), FileHelper.backupFilename(context, Constants.BackupType.ENCRYPTED)); - String password = settings.getBackupPasswordEnc(); + if (settings.isEncryptedBackupBroadcastEnabled()) { + if (!canSaveBackup(context)) + return; - if (password.isEmpty()) { - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_crypt_password_not_set); - return; - } + Uri savePath = Tools.buildUri(settings.getBackupDir(), FileHelper.backupFilename(context, Constants.BackupType.ENCRYPTED)); - SecretKey encryptionKey = null; + String password = settings.getBackupPasswordEnc(); - if (settings.getEncryption() == Constants.EncryptionType.KEYSTORE) { - encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(context, false); - } else { - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_custom_encryption_failed ); - return; - } + if (password.isEmpty()) { + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_crypt_password_not_set); + return; + } - if (Tools.isExternalStorageWritable()) { - ArrayList entries = DatabaseHelper.loadDatabase(context, encryptionKey); - String plain = DatabaseHelper.entriesToString(entries); + SecretKey encryptionKey = null; - boolean success = true; + if (settings.getEncryption() == Constants.EncryptionType.KEYSTORE) { + encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(context, false); + } else { + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_custom_encryption_failed); + return; + } - try { - SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password); - byte[] encrypted = EncryptionHelper.encrypt(key, plain.getBytes(StandardCharsets.UTF_8)); - FileHelper.writeBytesToFile(context, savePath, encrypted); - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_success, savePath.getPath()); - } catch (Exception e) { - e.printStackTrace(); - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_export_failed); + if (Tools.isExternalStorageWritable()) { + ArrayList entries = DatabaseHelper.loadDatabase(context, encryptionKey); + String plain = DatabaseHelper.entriesToString(entries); + + try { + SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password); + byte[] encrypted = EncryptionHelper.encrypt(key, plain.getBytes(StandardCharsets.UTF_8)); + FileHelper.writeBytesToFile(context, savePath, encrypted); + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_SUCCESS, R.string.backup_receiver_title_backup_success, savePath.getPath()); + } catch (Exception e) { + e.printStackTrace(); + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_export_failed); + } + } else { + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_storage_not_accessible); } } else { - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_storage_not_accessible); + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_encrypted_disabled); } } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Receivers/PlainTextBackupBroadcastReceiver.java b/app/src/main/java/org/shadowice/flocke/andotp/Receivers/PlainTextBackupBroadcastReceiver.java index 453fddc5..793676fa 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Receivers/PlainTextBackupBroadcastReceiver.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Receivers/PlainTextBackupBroadcastReceiver.java @@ -41,38 +41,41 @@ import java.util.ArrayList; import javax.crypto.SecretKey; -//Test with: adb shell am broadcast -n org.shadowice.flocke.andotp/.Receivers.PlainTextBackupBroadcastReceiver +// Use the following command to test in the dev version: +// adb shell am broadcast -a org.shadowice.flocke.andotp.broadcast.PLAIN_TEXT_BACKUP org.shadowice.flocke.andotp.dev public class PlainTextBackupBroadcastReceiver extends BackupBroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_disabled); - - /*if(!canSaveBackup(context)) - return; - Settings settings = new Settings(context); - Uri savePath = Tools.buildUri(settings.getBackupDir(), FileHelper.backupFilename(context, Constants.BackupType.PLAIN_TEXT)); + if (settings.isPlainTextBackupBroadcastEnabled()) { + if (!canSaveBackup(context)) + return; - SecretKey encryptionKey = null; + Uri savePath = Tools.buildUri(settings.getBackupDir(), FileHelper.backupFilename(context, Constants.BackupType.PLAIN_TEXT)); - if (settings.getEncryption() == Constants.EncryptionType.KEYSTORE) { - encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(context, false); - } else { - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_custom_encryption_failed); - return; - } + SecretKey encryptionKey = null; - if (Tools.isExternalStorageWritable()) { - ArrayList entries = DatabaseHelper.loadDatabase(context, encryptionKey); - - if (FileHelper.writeStringToFile(context, savePath, DatabaseHelper.entriesToString(entries))) { - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_success, savePath.getPath()); + if (settings.getEncryption() == Constants.EncryptionType.KEYSTORE) { + encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(context, false); } else { - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_export_failed); + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_custom_encryption_failed); + return; + } + + if (Tools.isExternalStorageWritable()) { + ArrayList entries = DatabaseHelper.loadDatabase(context, encryptionKey); + + if (FileHelper.writeStringToFile(context, savePath, DatabaseHelper.entriesToString(entries))) { + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_SUCCESS, R.string.backup_receiver_title_backup_success, savePath.getPath()); + } else { + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_export_failed); + } + } else { + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_storage_not_accessible); } } else { - NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_storage_not_accessible); - }*/ + NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_plain_disabled); + } } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java index 5c818cad..98012951 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java @@ -48,6 +48,10 @@ public class Constants { OR, AND, SINGLE } + public enum NotificationChannel { + BACKUP_FAILED, BACKUP_SUCCESS + } + // Intents (Format: A0x with A = parent Activity, x = number of the intent) public final static int INTENT_MAIN_AUTHENTICATE = 100; public final static int INTENT_MAIN_SETTINGS = 101; diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/NotificationHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/NotificationHelper.java index f4bdbf14..b05a773d 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/NotificationHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/NotificationHelper.java @@ -34,31 +34,63 @@ import org.shadowice.flocke.andotp.R; import static android.content.Context.NOTIFICATION_SERVICE; public class NotificationHelper { - public static void notify(Context context, int resIdTitle, int resIdBody) { - notify(context, resIdTitle, context.getText(resIdBody).toString()); + private static String channelId(Constants.NotificationChannel channel) { + return "andOTP_" + channel.name().toLowerCase(); } - public static void notify(Context context, int resIdTitle, String resBody) { - String channelId = "andOTP_channel"; - NotificationChannel channel = null; + private static void createNotificationChannel(Context context, Constants.NotificationChannel channel) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel notificationChannel = new NotificationChannel(channelId(channel), context.getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT); + switch(channel) { + case BACKUP_FAILED: + notificationChannel.setName(context.getString(R.string.notification_channel_name_backup_failed)); + notificationChannel.setDescription(context.getString(R.string.notification_channel_desc_backup_failed)); + notificationChannel.setImportance(NotificationManager.IMPORTANCE_HIGH); + break; + case BACKUP_SUCCESS: + notificationChannel.setName(context.getString(R.string.notification_channel_name_backup_success)); + notificationChannel.setDescription(context.getString(R.string.notification_channel_desc_backup_success)); + notificationChannel.setImportance(NotificationManager.IMPORTANCE_LOW); + break; + default: + break; + } + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); + notificationManager.createNotificationChannel(notificationChannel); + } + } + + public static void initializeNotificationChannels(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + for (Constants.NotificationChannel channel : Constants.NotificationChannel.values()) { + NotificationHelper.createNotificationChannel(context, channel); + } + } + } + + public static void notify(Context context, Constants.NotificationChannel channel, int resIdTitle, int resIdBody) { + notify(context, channel, resIdTitle, context.getText(resIdBody).toString()); + } + + public static void notify(Context context, Constants.NotificationChannel channel , int resIdTitle, String resBody) { NotificationCompat.Builder builder = new NotificationCompat.Builder(context, null) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle(context.getText(resIdTitle)) - .setContentText(resBody); + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(resBody)); - NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - channel = new NotificationChannel(channelId, context.getString(R.string.app_name), NotificationManager.IMPORTANCE_HIGH); - notificationManager.createNotificationChannel(channel); - } else { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { builder.setPriority(NotificationCompat.PRIORITY_HIGH); } - builder.setChannelId(channelId); + createNotificationChannel(context, channel); + builder.setChannelId(channelId(channel)); int notificationId = 1; + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(notificationId, builder.build()); } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java index fc6ce363..196cb09c 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java @@ -391,6 +391,18 @@ public class Settings { return password; } + public Set getBackupBroadcasts() { + return settings.getStringSet(getResString(R.string.settings_key_backup_broadcasts), Collections.emptySet()); + } + + public boolean isPlainTextBackupBroadcastEnabled() { + return getBackupBroadcasts().contains("plain"); + } + + public boolean isEncryptedBackupBroadcastEnabled() { + return getBackupBroadcasts().contains("encrypted"); + } + public String getOpenPGPProvider() { return getString(R.string.settings_key_openpgp_provider, ""); } diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index f488f388..20f76bf8 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -30,6 +30,7 @@ pref_backup_directory pref_backup_password pref_backup_password_enc + pref_backup_broadcasts pref_openpgp_provider pref_openpgp_keyid pref_openpgp_sign @@ -123,6 +124,11 @@ single + + plain + encrypted + + @string/settings_lang_sys_default CatalĂ  diff --git a/app/src/main/res/values/strings_backup.xml b/app/src/main/res/values/strings_backup.xml index 08540d7a..3a56a5e9 100644 --- a/app/src/main/res/values/strings_backup.xml +++ b/app/src/main/res/values/strings_backup.xml @@ -48,8 +48,10 @@ Backup failed Backup successful - Plain text backups are temporary disabled for security - reasons. Please wait for the next release. + Plain-text backups are currently not allowed, + please go to the Settings to enable them + Encrypted backups are currently not allowed, + please go to the Settings to enable them Read permission not granted, please do this before attempting backup @@ -58,6 +60,14 @@ Password/PIN based encryption not supported with broadcast backup + + Automatic backup failed + Automatic backup successful + + These notifications are shown when an + automatic backup has failed for some reason + These notifications are shown when an + automatic backup was successful Failed to create backup directory diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 5433efab..d7128ed0 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -28,6 +28,7 @@ Ask for filename Backup directory Backup password + Backup Broadcasts Select OpenPGP provider Select OpenPGP key Sign encrypted backups @@ -57,6 +58,9 @@ Set the password that is used to encrypt the backups + Select which backup types can be triggered by + 3rd-party apps using Broadcasts + Every encrypted backup is additionally signed with your key (requires password) Encrypted backups are only imported if they are @@ -166,6 +170,11 @@ Switch between tags (only allow one tag to be selected at a time) + + Plain-text backups + Encrypted backups + + System default diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 1192c623..00b9767d 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -134,6 +134,14 @@ android:title="@string/settings_title_backup_password" android:summary="@string/settings_desc_backup_password" /> + +