Fix backups
This commit is contained in:
parent
41cf364438
commit
494f9d66fc
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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<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)) {
|
||||
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);
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -166,6 +166,10 @@ public class OTPDatabase {
|
||||
loadedDatabase = null;
|
||||
}
|
||||
|
||||
public static void setLoadedDatabase(OTPDatabase loadedDatabase) {
|
||||
OTPDatabase.loadedDatabase = loadedDatabase;
|
||||
}
|
||||
|
||||
public static OTPDatabase getLoadedDatabase() {
|
||||
return loadedDatabase;
|
||||
}
|
||||
|
@ -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<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) {
|
||||
List<String> groups = new ArrayList<>(getGroups(ctx));
|
||||
groups.add(group);
|
||||
@ -62,28 +76,6 @@ public class SettingsUtil {
|
||||
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) {
|
||||
return ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".name", group);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user