Fix backups

This commit is contained in:
MrLetsplay 2023-09-26 20:52:44 +02:00
parent 41cf364438
commit 494f9d66fc
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
7 changed files with 195 additions and 59 deletions

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package com.cringe_studios.cringe_authenticator.util; package com.cringe_studios.cringe_authenticator.backup;
import android.content.Context; import android.content.Context;
import android.net.Uri; 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.CryptoException;
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters; 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.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -17,6 +25,7 @@ import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
@ -33,12 +42,20 @@ public class BackupUtil {
if(!OTPDatabase.isDatabaseLoaded()) throw new BackupException("Database is not loaded"); if(!OTPDatabase.isDatabaseLoaded()) throw new BackupException("Database is not loaded");
byte[] dbBytes = OTPDatabase.convertToEncryptedBytes(OTPDatabase.getLoadedDatabase(), key, parameters); byte[] dbBytes = OTPDatabase.convertToEncryptedBytes(OTPDatabase.getLoadedDatabase(), key, parameters);
JsonObject object = new JsonObject(); String database = Base64.encodeToString(dbBytes, Base64.DEFAULT);
object.add("parameters", SettingsUtil.GSON.toJsonTree(parameters));
object.addProperty("database", Base64.encodeToString(dbBytes, Base64.DEFAULT)); List<String> 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)) { try(OutputStream out = context.getContentResolver().openOutputStream(backupFile)) {
if(out == null) throw new BackupException("Failed to write backup"); 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) { }catch(IOException e) {
throw new BackupException(e); throw new BackupException(e);
} }
@ -48,21 +65,23 @@ public class BackupUtil {
try(InputStream in = context.getContentResolver().openInputStream(backupFile)) { try(InputStream in = context.getContentResolver().openInputStream(backupFile)) {
if(in == null) throw new BackupException("Failed to read backup file"); if(in == null) throw new BackupException("Failed to read backup file");
byte[] backupBytes = IOUtil.readBytes(in); byte[] backupBytes = IOUtil.readBytes(in);
JsonObject object = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), JsonObject.class); BackupData data = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), BackupData.class);
return SettingsUtil.GSON.fromJson(object.get("parameters"), CryptoParameters.class); // TODO: check if params are valid 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) { } catch (IOException e) {
throw new BackupException(e); throw new BackupException(e);
} }
} }
public static OTPDatabase loadBackup(Context context, Uri backupFile, SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException { public static BackupData loadBackup(Context context, Uri backupFile) throws BackupException {
try (InputStream in = context.getContentResolver().openInputStream(backupFile)) { try (InputStream in = context.getContentResolver().openInputStream(backupFile)) {
if (in == null) throw new BackupException("Failed to read backup file"); if (in == null) throw new BackupException("Failed to read backup file");
byte[] backupBytes = IOUtil.readBytes(in); byte[] backupBytes = IOUtil.readBytes(in);
JsonObject object = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), JsonObject.class); BackupData data = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), BackupData.class);
JsonElement db = object.get("database"); if(!data.isValid()) throw new BackupException("Invalid backup data"); // TODO: more info on backup errors
if(db == null) throw new BackupException("Invalid backup file"); return data;
return OTPDatabase.loadFromEncryptedBytes(Base64.decode(db.getAsString(), Base64.DEFAULT), key, parameters);
} catch (JsonSyntaxException e) { } catch (JsonSyntaxException e) {
throw new BackupException("Invalid JSON", e); throw new BackupException("Invalid JSON", e);
} catch (IOException e) { } catch (IOException e) {

View File

@ -75,6 +75,19 @@ public class CryptoParameters {
return encryptionSaltLength; 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() { public static CryptoParameters createNew() {
CryptoParameters params = new CryptoParameters(); CryptoParameters params = new CryptoParameters();
byte[] salt = Crypto.generateSalt(params); byte[] salt = Crypto.generateSalt(params);

View File

@ -1,6 +1,5 @@
package com.cringe_studios.cringe_authenticator.fragment; package com.cringe_studios.cringe_authenticator.fragment;
import android.app.AlertDialog;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; 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.MainActivity;
import com.cringe_studios.cringe_authenticator.R; 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.BiometricKey;
import com.cringe_studios.cringe_authenticator.crypto.Crypto; import com.cringe_studios.cringe_authenticator.crypto.Crypto;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException; 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.databinding.FragmentSettingsBinding;
import com.cringe_studios.cringe_authenticator.util.Appearance; import com.cringe_studios.cringe_authenticator.util.Appearance;
import com.cringe_studios.cringe_authenticator.util.BackupException; 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.BiometricUtil;
import com.cringe_studios.cringe_authenticator.util.DialogUtil; import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.OTPDatabase; 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.StyledDialogBuilder;
import com.cringe_studios.cringe_authenticator.util.Theme; 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.Arrays;
import java.util.Locale; import java.util.Locale;
@ -265,11 +262,18 @@ public class SettingsFragment extends NamedFragment {
private void loadBackup(Uri uri) { private void loadBackup(Uri uri) {
OTPDatabase.promptLoadDatabase(requireActivity(), () -> { OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
if(SettingsUtil.isDatabaseEncrypted(requireContext())) {
try { try {
SecretKey key = OTPDatabase.getLoadedKey(); SecretKey key = OTPDatabase.getLoadedKey();
CryptoParameters parameters = SettingsUtil.getCryptoParameters(requireContext()); CryptoParameters parameters = SettingsUtil.getCryptoParameters(requireContext());
loadBackup(uri, key, parameters); loadBackup(uri, key, parameters);
} catch (CryptoException e) { } catch (CryptoException ignored) { // Load with password
} catch (BackupException | OTPDatabaseException e) {
DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e);
return;
}
}
DialogUtil.showInputPasswordDialog(requireContext(), password -> { DialogUtil.showInputPasswordDialog(requireContext(), password -> {
try { try {
CryptoParameters parameters = BackupUtil.loadParametersFromBackup(requireContext(), uri); CryptoParameters parameters = BackupUtil.loadParametersFromBackup(requireContext(), uri);
@ -279,15 +283,25 @@ public class SettingsFragment extends NamedFragment {
DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e2); DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e2);
} }
}, null); }, null);
} catch(BackupException | OTPDatabaseException e) {
DialogUtil.showErrorDialog(requireContext(), "Failed to load backup", e);
}
}, null); }, null);
} }
private void loadBackup(Uri uri, SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException { private void loadBackup(Uri uri, SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException {
OTPDatabase db = BackupUtil.loadBackup(requireContext(), uri, key, parameters); BackupData data = BackupUtil.loadBackup(requireContext(), uri);
DialogUtil.showErrorDialog(requireContext(), "Success: " + db); 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 @Override

View File

@ -166,6 +166,10 @@ public class OTPDatabase {
loadedDatabase = null; loadedDatabase = null;
} }
public static void setLoadedDatabase(OTPDatabase loadedDatabase) {
OTPDatabase.loadedDatabase = loadedDatabase;
}
public static OTPDatabase getLoadedDatabase() { public static OTPDatabase getLoadedDatabase() {
return loadedDatabase; return loadedDatabase;
} }

View File

@ -5,6 +5,7 @@ import android.content.SharedPreferences;
import android.util.Base64; import android.util.Base64;
import com.cringe_studios.cringe_authenticator.R; 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.BiometricKey;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException; import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters; import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
@ -42,6 +43,19 @@ public class SettingsUtil {
prefs.edit().putString("groups", GSON.toJson(groups)).apply(); prefs.edit().putString("groups", GSON.toJson(groups)).apply();
} }
public static void restoreGroups(Context ctx, BackupGroup[] groups) {
List<String> oldGroups = getGroups(ctx);
for(String group : oldGroups) removeGroup(ctx, group);
List<String> 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) { public static void addGroup(Context ctx, String group, String groupName) {
List<String> groups = new ArrayList<>(getGroups(ctx)); List<String> groups = new ArrayList<>(getGroups(ctx));
groups.add(group); groups.add(group);
@ -62,28 +76,6 @@ public class SettingsUtil {
deleteGroupData(ctx, group); deleteGroupData(ctx, group);
} }
/*public static List<OTPData> 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<OTPData> 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<OTPData> 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) { public static String getGroupName(Context ctx, String group) {
return ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".name", group); return ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".name", group);
} }