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.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);

View file

@ -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;
}

View file

@ -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<Entry> 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<Entry> 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);
}
}
}

View file

@ -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<Entry> 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<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 {
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
}
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;

View file

@ -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());
}
}

View file

@ -391,6 +391,18 @@ public class Settings {
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() {
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_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_broadcasts" translatable="false">pref_backup_broadcasts</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_sign" translatable="false">pref_openpgp_sign</string>
@ -123,6 +124,11 @@
<item>single</item>
</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">
<item>@string/settings_lang_sys_default</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_success">Backup successful</string>
<string name="backup_receiver_disabled">Plain text backups are temporary disabled for security
reasons. Please wait for the next release.</string>
<string name="backup_receiver_plain_disabled">Plain-text backups are currently not allowed,
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
this before attempting backup</string>
@ -58,6 +60,14 @@
<string name="backup_receiver_custom_encryption_failed">Password/PIN based encryption not
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 -->
<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_directory">Backup directory</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_keyid">Select OpenPGP key</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
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
your key (requires password)</string>
<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>
</string-array>
<string-array name="settings_entries_backup_broadcasts">
<item>Plain-text backups</item>
<item>Encrypted backups</item>
</string-array>
<!-- Special -->
<string name="settings_lang_sys_default">System default</string>

View file

@ -134,6 +134,14 @@
android:title="@string/settings_title_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
android:key="@string/settings_key_openpgp_provider"
android:title="@string/settings_title_openpgp_provider" />