From 494f9d66fc183a04f02be7b11942c9afedf735cb Mon Sep 17 00:00:00 2001 From: MrLetsplay Date: Tue, 26 Sep 2023 20:52:44 +0200 Subject: [PATCH] Fix backups --- .../backup/BackupData.java | 65 +++++++++++++++++++ .../backup/BackupGroup.java | 29 +++++++++ .../{util => backup}/BackupUtil.java | 47 ++++++++++---- .../crypto/CryptoParameters.java | 13 ++++ .../fragment/SettingsFragment.java | 60 ++++++++++------- .../util/OTPDatabase.java | 4 ++ .../util/SettingsUtil.java | 36 ++++------ 7 files changed, 195 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupData.java create mode 100644 app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupGroup.java rename app/src/main/java/com/cringe_studios/cringe_authenticator/{util => backup}/BackupUtil.java (54%) diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupData.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupData.java new file mode 100644 index 0000000..fae127a --- /dev/null +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupData.java @@ -0,0 +1,65 @@ +package com.cringe_studios.cringe_authenticator.backup; + +import android.util.Base64; +import android.util.Log; + +import com.cringe_studios.cringe_authenticator.crypto.CryptoException; +import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters; +import com.cringe_studios.cringe_authenticator.model.OTPData; +import com.cringe_studios.cringe_authenticator.proto.OTPMigration; +import com.cringe_studios.cringe_authenticator.util.BackupException; +import com.cringe_studios.cringe_authenticator.util.OTPDatabase; +import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException; +import com.cringe_studios.cringe_authenticator.util.SettingsUtil; + +import javax.crypto.SecretKey; + +public class BackupData { + + private CryptoParameters parameters; + + private String database; + + private BackupGroup[] groups; + + private BackupData() {} + + public BackupData(CryptoParameters parameters, String database, BackupGroup[] groups) { + this.parameters = parameters; + this.database = database; + this.groups = groups; + } + + public CryptoParameters getParameters() { + return parameters; + } + + public String getDatabase() { + return database; + } + + public OTPDatabase loadDatabase(SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException { + try { + return OTPDatabase.loadFromEncryptedBytes(Base64.decode(database, Base64.DEFAULT), key, parameters); + }catch(IllegalArgumentException e) { + throw new BackupException(e); + } + } + + public BackupGroup[] getGroups() { + return groups; + } + + public boolean isValid() { + if(parameters == null || database == null || groups == null) return false; + + if(!parameters.isValid()) return false; + + for(BackupGroup group : groups) { + if(!group.isValid()) return false; + } + + return true; + } + +} diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupGroup.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupGroup.java new file mode 100644 index 0000000..7aa19a2 --- /dev/null +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupGroup.java @@ -0,0 +1,29 @@ +package com.cringe_studios.cringe_authenticator.backup; + +import org.jetbrains.annotations.Contract; + +public class BackupGroup { + + public String id; + public String name; + + private BackupGroup() {} + + public BackupGroup(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public boolean isValid() { + return id != null && name != null; + } + +} diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/BackupUtil.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupUtil.java similarity index 54% rename from app/src/main/java/com/cringe_studios/cringe_authenticator/util/BackupUtil.java rename to app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupUtil.java index 596cb4f..26c79e2 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/BackupUtil.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/backup/BackupUtil.java @@ -1,4 +1,4 @@ -package com.cringe_studios.cringe_authenticator.util; +package com.cringe_studios.cringe_authenticator.backup; import android.content.Context; import android.net.Uri; @@ -6,10 +6,18 @@ import android.util.Base64; import com.cringe_studios.cringe_authenticator.crypto.CryptoException; import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters; +import com.cringe_studios.cringe_authenticator.util.BackupException; +import com.cringe_studios.cringe_authenticator.util.IOUtil; +import com.cringe_studios.cringe_authenticator.util.OTPDatabase; +import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException; +import com.cringe_studios.cringe_authenticator.util.SettingsUtil; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import org.json.JSONObject; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -17,6 +25,7 @@ import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; +import java.util.List; import java.util.Locale; import javax.crypto.SecretKey; @@ -33,12 +42,20 @@ public class BackupUtil { if(!OTPDatabase.isDatabaseLoaded()) throw new BackupException("Database is not loaded"); byte[] dbBytes = OTPDatabase.convertToEncryptedBytes(OTPDatabase.getLoadedDatabase(), key, parameters); - JsonObject object = new JsonObject(); - object.add("parameters", SettingsUtil.GSON.toJsonTree(parameters)); - object.addProperty("database", Base64.encodeToString(dbBytes, Base64.DEFAULT)); + String database = Base64.encodeToString(dbBytes, Base64.DEFAULT); + + List groups = SettingsUtil.getGroups(context); + BackupGroup[] groupsArray = new BackupGroup[groups.size()]; + for(int i = 0; i < groups.size(); i++) { + String group = groups.get(i); + groupsArray[i] = new BackupGroup(group, SettingsUtil.getGroupName(context, group)); + } + + BackupData data = new BackupData(parameters, database, groupsArray); + try(OutputStream out = context.getContentResolver().openOutputStream(backupFile)) { if(out == null) throw new BackupException("Failed to write backup"); - out.write(object.toString().getBytes(StandardCharsets.UTF_8)); + out.write(SettingsUtil.GSON.toJson(data).getBytes(StandardCharsets.UTF_8)); }catch(IOException e) { throw new BackupException(e); } @@ -48,22 +65,24 @@ public class BackupUtil { try(InputStream in = context.getContentResolver().openInputStream(backupFile)) { if(in == null) throw new BackupException("Failed to read backup file"); byte[] backupBytes = IOUtil.readBytes(in); - JsonObject object = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), JsonObject.class); - return SettingsUtil.GSON.fromJson(object.get("parameters"), CryptoParameters.class); // TODO: check if params are valid + BackupData data = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), BackupData.class); + if(!data.getParameters().isValid()) throw new BackupException("Invalid crypto parameters"); + return data.getParameters(); + } catch (JsonSyntaxException e) { + throw new BackupException("Invalid JSON", e); } catch (IOException e) { throw new BackupException(e); } } - public static OTPDatabase loadBackup(Context context, Uri backupFile, SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException { - try(InputStream in = context.getContentResolver().openInputStream(backupFile)) { + public static BackupData loadBackup(Context context, Uri backupFile) throws BackupException { + try (InputStream in = context.getContentResolver().openInputStream(backupFile)) { if (in == null) throw new BackupException("Failed to read backup file"); byte[] backupBytes = IOUtil.readBytes(in); - JsonObject object = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), JsonObject.class); - JsonElement db = object.get("database"); - if(db == null) throw new BackupException("Invalid backup file"); - return OTPDatabase.loadFromEncryptedBytes(Base64.decode(db.getAsString(), Base64.DEFAULT), key, parameters); - } catch(JsonSyntaxException e) { + BackupData data = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), BackupData.class); + if(!data.isValid()) throw new BackupException("Invalid backup data"); // TODO: more info on backup errors + return data; + } catch (JsonSyntaxException e) { throw new BackupException("Invalid JSON", e); } catch (IOException e) { throw new BackupException(e); diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/crypto/CryptoParameters.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/crypto/CryptoParameters.java index 1bd24e0..269207f 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/crypto/CryptoParameters.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/crypto/CryptoParameters.java @@ -75,6 +75,19 @@ public class CryptoParameters { return encryptionSaltLength; } + public boolean isValid() { + return hashType != null + && argon2Version > 0 + && argon2Iterations > 0 + && argon2Memory > 0 + && argon2Parallelism > 0 + && encryptionAlgorithm != null + && encryptionGCMTagLength > 0 + && encryptionIVLength > 0 + && encryptionAESKeyLength > 0 + && encryptionSaltLength > 0; + } + public static CryptoParameters createNew() { CryptoParameters params = new CryptoParameters(); byte[] salt = Crypto.generateSalt(params); diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/SettingsFragment.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/SettingsFragment.java index 9742f8c..a580bbe 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/SettingsFragment.java @@ -1,6 +1,5 @@ package com.cringe_studios.cringe_authenticator.fragment; -import android.app.AlertDialog; import android.net.Uri; import android.os.Bundle; import android.util.Log; @@ -15,6 +14,7 @@ import androidx.annotation.Nullable; import com.cringe_studios.cringe_authenticator.MainActivity; import com.cringe_studios.cringe_authenticator.R; +import com.cringe_studios.cringe_authenticator.backup.BackupData; import com.cringe_studios.cringe_authenticator.crypto.BiometricKey; import com.cringe_studios.cringe_authenticator.crypto.Crypto; import com.cringe_studios.cringe_authenticator.crypto.CryptoException; @@ -22,7 +22,7 @@ import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters; import com.cringe_studios.cringe_authenticator.databinding.FragmentSettingsBinding; import com.cringe_studios.cringe_authenticator.util.Appearance; import com.cringe_studios.cringe_authenticator.util.BackupException; -import com.cringe_studios.cringe_authenticator.util.BackupUtil; +import com.cringe_studios.cringe_authenticator.backup.BackupUtil; import com.cringe_studios.cringe_authenticator.util.BiometricUtil; import com.cringe_studios.cringe_authenticator.util.DialogUtil; import com.cringe_studios.cringe_authenticator.util.OTPDatabase; @@ -31,9 +31,6 @@ import com.cringe_studios.cringe_authenticator.util.SettingsUtil; import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder; import com.cringe_studios.cringe_authenticator.util.Theme; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; import java.util.Arrays; import java.util.Locale; @@ -265,29 +262,46 @@ public class SettingsFragment extends NamedFragment { private void loadBackup(Uri uri) { OTPDatabase.promptLoadDatabase(requireActivity(), () -> { - try { - SecretKey key = OTPDatabase.getLoadedKey(); - CryptoParameters parameters = SettingsUtil.getCryptoParameters(requireContext()); - loadBackup(uri, key, parameters); - } catch (CryptoException e) { - DialogUtil.showInputPasswordDialog(requireContext(), password -> { - try { - CryptoParameters parameters = BackupUtil.loadParametersFromBackup(requireContext(), uri); - SecretKey key = Crypto.generateKey(parameters, password); - loadBackup(uri, key, parameters); - } catch (BackupException | OTPDatabaseException | CryptoException e2) { - DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e2); - } - }, null); - } catch(BackupException | OTPDatabaseException e) { - DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e); + if(SettingsUtil.isDatabaseEncrypted(requireContext())) { + try { + SecretKey key = OTPDatabase.getLoadedKey(); + CryptoParameters parameters = SettingsUtil.getCryptoParameters(requireContext()); + loadBackup(uri, key, parameters); + } catch (CryptoException ignored) { // Load with password + } catch (BackupException | OTPDatabaseException e) { + DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e); + return; + } } + + DialogUtil.showInputPasswordDialog(requireContext(), password -> { + try { + CryptoParameters parameters = BackupUtil.loadParametersFromBackup(requireContext(), uri); + SecretKey key = Crypto.generateKey(parameters, password); + loadBackup(uri, key, parameters); + } catch (BackupException | OTPDatabaseException | CryptoException e2) { + DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e2); + } + }, null); }, null); } private void loadBackup(Uri uri, SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException { - OTPDatabase db = BackupUtil.loadBackup(requireContext(), uri, key, parameters); - DialogUtil.showErrorDialog(requireContext(), "Success: " + db); + BackupData data = BackupUtil.loadBackup(requireContext(), uri); + OTPDatabase db = data.loadDatabase(key, parameters); + //DialogUtil.showErrorDialog(requireContext(), "Success: " + db); + // TODO: prompt user that all current data will be deleted + OTPDatabase.promptLoadDatabase(requireActivity(), () -> { + OTPDatabase oldDatabase = OTPDatabase.getLoadedDatabase(); + try { + SettingsUtil.restoreGroups(requireContext(), data.getGroups()); + OTPDatabase.setLoadedDatabase(db); + OTPDatabase.saveDatabase(requireContext(), SettingsUtil.getCryptoParameters(requireContext())); + } catch (OTPDatabaseException | CryptoException e) { + OTPDatabase.setLoadedDatabase(oldDatabase); + throw new RuntimeException(e); + } + }, null); } @Override diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/OTPDatabase.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/OTPDatabase.java index d9372f0..10a9dcb 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/OTPDatabase.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/OTPDatabase.java @@ -166,6 +166,10 @@ public class OTPDatabase { loadedDatabase = null; } + public static void setLoadedDatabase(OTPDatabase loadedDatabase) { + OTPDatabase.loadedDatabase = loadedDatabase; + } + public static OTPDatabase getLoadedDatabase() { return loadedDatabase; } diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/SettingsUtil.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/SettingsUtil.java index b944e31..4d69639 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/SettingsUtil.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/SettingsUtil.java @@ -5,6 +5,7 @@ import android.content.SharedPreferences; import android.util.Base64; import com.cringe_studios.cringe_authenticator.R; +import com.cringe_studios.cringe_authenticator.backup.BackupGroup; import com.cringe_studios.cringe_authenticator.crypto.BiometricKey; import com.cringe_studios.cringe_authenticator.crypto.CryptoException; import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters; @@ -42,6 +43,19 @@ public class SettingsUtil { prefs.edit().putString("groups", GSON.toJson(groups)).apply(); } + public static void restoreGroups(Context ctx, BackupGroup[] groups) { + List oldGroups = getGroups(ctx); + for(String group : oldGroups) removeGroup(ctx, group); + + List newGroups = new ArrayList<>(); + for(BackupGroup group : groups) { + newGroups.add(group.getId()); + setGroupName(ctx, group.getId(), group.getName()); + } + + setGroups(ctx, newGroups); + } + public static void addGroup(Context ctx, String group, String groupName) { List groups = new ArrayList<>(getGroups(ctx)); groups.add(group); @@ -62,28 +76,6 @@ public class SettingsUtil { deleteGroupData(ctx, group); } - /*public static List getOTPs(Context ctx, String group) { - String currentOTPs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".otps", "[]"); - return Arrays.asList(GSON.fromJson(currentOTPs, OTPData[].class)); - } - - public static void addOTP(Context ctx, String group, @NonNull OTPData data) { - // TODO: check for code with same name - - List otps = new ArrayList<>(getOTPs(ctx, group)); - otps.add(data); - - ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit() - .putString("group." + group + ".otps", GSON.toJson(otps.toArray(new OTPData[0]))) - .apply(); - } - - public static void updateOTPs(Context ctx, String group, List otps) { - ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit() - .putString("group." + group + ".otps", GSON.toJson(otps.toArray(new OTPData[0]))) - .apply(); - }*/ - public static String getGroupName(Context ctx, String group) { return ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".name", group); }