Broadcast backup improvements

* Add setting to enable backup types as discusses in #157
 * Add different notification channels
This commit is contained in:
Jakob Nixdorf 2018-03-15 11:20:44 +01:00
parent 8563679b17
commit 76edcb900a
No known key found for this signature in database
GPG key ID: BE99BF86574A7DBC
11 changed files with 160 additions and 74 deletions

View file

@ -60,6 +60,7 @@ import org.shadowice.flocke.andotp.R;
import org.shadowice.flocke.andotp.Utilities.Constants; import org.shadowice.flocke.andotp.Utilities.Constants;
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; 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.Utilities.TokenCalculator;
import org.shadowice.flocke.andotp.View.EntriesCardAdapter; import org.shadowice.flocke.andotp.View.EntriesCardAdapter;
import org.shadowice.flocke.andotp.View.FloatingActionMenu; import org.shadowice.flocke.andotp.View.FloatingActionMenu;
@ -222,6 +223,7 @@ public class MainActivity extends BaseActivity
ItemTouchHelper touchHelper = new ItemTouchHelper(touchHelperCallback); ItemTouchHelper touchHelper = new ItemTouchHelper(touchHelperCallback);
touchHelper.attachToRecyclerView(recList); touchHelper.attachToRecyclerView(recList);
NotificationHelper.initializeNotificationChannels(this);
restoreSortMode(); restoreSortMode();
float durationScale = android.provider.Settings.Global.getFloat(this.getContentResolver(), android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 0); float durationScale = android.provider.Settings.Global.getFloat(this.getContentResolver(), android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 0);

View file

@ -24,33 +24,28 @@
package org.shadowice.flocke.andotp.Receivers; package org.shadowice.flocke.andotp.Receivers;
import android.Manifest; import android.Manifest;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import org.shadowice.flocke.andotp.R; 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.NotificationHelper;
import org.shadowice.flocke.andotp.Utilities.Settings; import org.shadowice.flocke.andotp.Utilities.Settings;
import java.io.File; import java.io.File;
import static android.content.Context.NOTIFICATION_SERVICE;
public abstract class BackupBroadcastReceiver extends BroadcastReceiver { public abstract class BackupBroadcastReceiver extends BroadcastReceiver {
protected boolean canSaveBackup(Context context) { protected boolean canSaveBackup(Context context) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 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; return false;
} }
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 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; return false;
} }

View file

@ -43,48 +43,53 @@ import java.util.ArrayList;
import javax.crypto.SecretKey; 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 { public class EncryptedBackupBroadcastReceiver extends BackupBroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if(!canSaveBackup(context))
return;
Settings settings = new Settings(context); 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()) { Uri savePath = Tools.buildUri(settings.getBackupDir(), FileHelper.backupFilename(context, Constants.BackupType.ENCRYPTED));
NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_crypt_password_not_set);
return;
}
SecretKey encryptionKey = null; String password = settings.getBackupPasswordEnc();
if (settings.getEncryption() == Constants.EncryptionType.KEYSTORE) { if (password.isEmpty()) {
encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(context, false); NotificationHelper.notify(context, Constants.NotificationChannel.BACKUP_FAILED, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_crypt_password_not_set);
} else { return;
NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_receiver_custom_encryption_failed ); }
return;
}
if (Tools.isExternalStorageWritable()) { SecretKey encryptionKey = null;
ArrayList<Entry> entries = DatabaseHelper.loadDatabase(context, encryptionKey);
String plain = DatabaseHelper.entriesToString(entries);
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 { if (Tools.isExternalStorageWritable()) {
SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password); ArrayList<Entry> entries = DatabaseHelper.loadDatabase(context, encryptionKey);
byte[] encrypted = EncryptionHelper.encrypt(key, plain.getBytes(StandardCharsets.UTF_8)); String plain = DatabaseHelper.entriesToString(entries);
FileHelper.writeBytesToFile(context, savePath, encrypted);
NotificationHelper.notify(context, R.string.backup_receiver_title_backup_success, savePath.getPath()); try {
} catch (Exception e) { SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password);
e.printStackTrace(); byte[] encrypted = EncryptionHelper.encrypt(key, plain.getBytes(StandardCharsets.UTF_8));
NotificationHelper.notify(context, R.string.backup_receiver_title_backup_failed, R.string.backup_toast_export_failed); 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 { } 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);
} }
} }
} }

View file

@ -41,38 +41,41 @@ import java.util.ArrayList;
import javax.crypto.SecretKey; 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 { public class PlainTextBackupBroadcastReceiver extends BackupBroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { 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); 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) { SecretKey encryptionKey = null;
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 (Tools.isExternalStorageWritable()) { if (settings.getEncryption() == Constants.EncryptionType.KEYSTORE) {
ArrayList<Entry> entries = DatabaseHelper.loadDatabase(context, encryptionKey); encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(context, false);
if (FileHelper.writeStringToFile(context, savePath, DatabaseHelper.entriesToString(entries))) {
NotificationHelper.notify(context, R.string.backup_receiver_title_backup_success, savePath.getPath());
} else { } 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<Entry> 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 { } 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);
}*/ }
} }
} }

View file

@ -48,6 +48,10 @@ public class Constants {
OR, AND, SINGLE OR, AND, SINGLE
} }
public enum NotificationChannel {
BACKUP_FAILED, BACKUP_SUCCESS
}
// Intents (Format: A0x with A = parent Activity, x = number of the intent) // 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_AUTHENTICATE = 100;
public final static int INTENT_MAIN_SETTINGS = 101; public final static int INTENT_MAIN_SETTINGS = 101;

View file

@ -34,31 +34,63 @@ import org.shadowice.flocke.andotp.R;
import static android.content.Context.NOTIFICATION_SERVICE; import static android.content.Context.NOTIFICATION_SERVICE;
public class NotificationHelper { public class NotificationHelper {
public static void notify(Context context, int resIdTitle, int resIdBody) { private static String channelId(Constants.NotificationChannel channel) {
notify(context, resIdTitle, context.getText(resIdBody).toString()); return "andOTP_" + channel.name().toLowerCase();
} }
public static void notify(Context context, int resIdTitle, String resBody) { private static void createNotificationChannel(Context context, Constants.NotificationChannel channel) {
String channelId = "andOTP_channel"; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = null; 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) NotificationCompat.Builder builder = new NotificationCompat.Builder(context, null)
.setSmallIcon(R.mipmap.ic_launcher) .setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(context.getText(resIdTitle)) .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) {
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 {
builder.setPriority(NotificationCompat.PRIORITY_HIGH); builder.setPriority(NotificationCompat.PRIORITY_HIGH);
} }
builder.setChannelId(channelId); createNotificationChannel(context, channel);
builder.setChannelId(channelId(channel));
int notificationId = 1; int notificationId = 1;
NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(notificationId, builder.build()); notificationManager.notify(notificationId, builder.build());
} }
} }

View file

@ -391,6 +391,18 @@ public class Settings {
return password; return password;
} }
public Set<String> getBackupBroadcasts() {
return settings.getStringSet(getResString(R.string.settings_key_backup_broadcasts), Collections.<String>emptySet());
}
public boolean isPlainTextBackupBroadcastEnabled() {
return getBackupBroadcasts().contains("plain");
}
public boolean isEncryptedBackupBroadcastEnabled() {
return getBackupBroadcasts().contains("encrypted");
}
public String getOpenPGPProvider() { public String getOpenPGPProvider() {
return getString(R.string.settings_key_openpgp_provider, ""); return getString(R.string.settings_key_openpgp_provider, "");
} }

View file

@ -30,6 +30,7 @@
<string name="settings_key_backup_directory" translatable="false">pref_backup_directory</string> <string name="settings_key_backup_directory" translatable="false">pref_backup_directory</string>
<string name="settings_key_backup_password" translatable="false">pref_backup_password</string> <!-- Deprecated --> <string name="settings_key_backup_password" translatable="false">pref_backup_password</string> <!-- Deprecated -->
<string name="settings_key_backup_password_enc" translatable="false">pref_backup_password_enc</string> <string name="settings_key_backup_password_enc" translatable="false">pref_backup_password_enc</string>
<string name="settings_key_backup_broadcasts" translatable="false">pref_backup_broadcasts</string>
<string name="settings_key_openpgp_provider" translatable="false">pref_openpgp_provider</string> <string name="settings_key_openpgp_provider" translatable="false">pref_openpgp_provider</string>
<string name="settings_key_openpgp_keyid" translatable="false">pref_openpgp_keyid</string> <string name="settings_key_openpgp_keyid" translatable="false">pref_openpgp_keyid</string>
<string name="settings_key_openpgp_sign" translatable="false">pref_openpgp_sign</string> <string name="settings_key_openpgp_sign" translatable="false">pref_openpgp_sign</string>
@ -123,6 +124,11 @@
<item>single</item> <item>single</item>
</string-array> </string-array>
<string-array name="settings_values_backup_broadcasts" translatable="false">
<item>plain</item>
<item>encrypted</item>
</string-array>
<string-array name="settings_entries_lang" translatable="false"> <string-array name="settings_entries_lang" translatable="false">
<item>@string/settings_lang_sys_default</item> <item>@string/settings_lang_sys_default</item>
<item>Català</item> <item>Català</item>

View file

@ -48,8 +48,10 @@
<string name="backup_receiver_title_backup_failed">Backup failed</string> <string name="backup_receiver_title_backup_failed">Backup failed</string>
<string name="backup_receiver_title_backup_success">Backup successful</string> <string name="backup_receiver_title_backup_success">Backup successful</string>
<string name="backup_receiver_disabled">Plain text backups are temporary disabled for security <string name="backup_receiver_plain_disabled">Plain-text backups are currently not allowed,
reasons. Please wait for the next release.</string> please go to the Settings to enable them</string>
<string name="backup_receiver_encrypted_disabled">Encrypted backups are currently not allowed,
please go to the Settings to enable them</string>
<string name="backup_receiver_read_permission_failed">Read permission not granted, please do <string name="backup_receiver_read_permission_failed">Read permission not granted, please do
this before attempting backup</string> this before attempting backup</string>
@ -58,6 +60,14 @@
<string name="backup_receiver_custom_encryption_failed">Password/PIN based encryption not <string name="backup_receiver_custom_encryption_failed">Password/PIN based encryption not
supported with broadcast backup</string> supported with broadcast backup</string>
<!-- Notification channels -->
<string name="notification_channel_name_backup_failed">Automatic backup failed</string>
<string name="notification_channel_name_backup_success">Automatic backup successful</string>
<string name="notification_channel_desc_backup_failed">These notifications are shown when an
automatic backup has failed for some reason</string>
<string name="notification_channel_desc_backup_success">These notifications are shown when an
automatic backup was successful</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="backup_toast_mkdir_failed">Failed to create backup directory</string> <string name="backup_toast_mkdir_failed">Failed to create backup directory</string>

View file

@ -28,6 +28,7 @@
<string name="settings_title_backup_ask">Ask for filename</string> <string name="settings_title_backup_ask">Ask for filename</string>
<string name="settings_title_backup_directory">Backup directory</string> <string name="settings_title_backup_directory">Backup directory</string>
<string name="settings_title_backup_password">Backup password</string> <string name="settings_title_backup_password">Backup password</string>
<string name="settings_title_backup_broadcasts">Backup Broadcasts</string>
<string name="settings_title_openpgp_provider">Select OpenPGP provider</string> <string name="settings_title_openpgp_provider">Select OpenPGP provider</string>
<string name="settings_title_openpgp_keyid">Select OpenPGP key</string> <string name="settings_title_openpgp_keyid">Select OpenPGP key</string>
<string name="settings_title_openpgp_sign">Sign encrypted backups</string> <string name="settings_title_openpgp_sign">Sign encrypted backups</string>
@ -57,6 +58,9 @@
<string name="settings_desc_backup_password">Set the password that is used to encrypt the <string name="settings_desc_backup_password">Set the password that is used to encrypt the
backups</string> backups</string>
<string name="settings_desc_backup_broadcasts">Select which backup types can be triggered by
3rd-party apps using Broadcasts</string>
<string name="settings_desc_openpgp_sign">Every encrypted backup is additionally signed with <string name="settings_desc_openpgp_sign">Every encrypted backup is additionally signed with
your key (requires password)</string> your key (requires password)</string>
<string name="settings_desc_openpgp_verify">Encrypted backups are only imported if they are <string name="settings_desc_openpgp_verify">Encrypted backups are only imported if they are
@ -166,6 +170,11 @@
<item>Switch between tags (only allow one tag to be selected at a time)</item> <item>Switch between tags (only allow one tag to be selected at a time)</item>
</string-array> </string-array>
<string-array name="settings_entries_backup_broadcasts">
<item>Plain-text backups</item>
<item>Encrypted backups</item>
</string-array>
<!-- Special --> <!-- Special -->
<string name="settings_lang_sys_default">System default</string> <string name="settings_lang_sys_default">System default</string>

View file

@ -134,6 +134,14 @@
android:title="@string/settings_title_backup_password" android:title="@string/settings_title_backup_password"
android:summary="@string/settings_desc_backup_password" /> android:summary="@string/settings_desc_backup_password" />
<MultiSelectListPreference
android:key="@string/settings_key_backup_broadcasts"
android:title="@string/settings_title_backup_broadcasts"
android:summary="@string/settings_desc_backup_broadcasts"
android:entries="@array/settings_entries_backup_broadcasts"
android:entryValues="@array/settings_values_backup_broadcasts"
android:defaultValue="@array/settings_empty_array" />
<org.openintents.openpgp.util.OpenPgpAppPreference <org.openintents.openpgp.util.OpenPgpAppPreference
android:key="@string/settings_key_openpgp_provider" android:key="@string/settings_key_openpgp_provider"
android:title="@string/settings_title_openpgp_provider" /> android:title="@string/settings_title_openpgp_provider" />