Implement backups (WIP), Move add OTP to menu
This commit is contained in:
parent
ff50e68e90
commit
2045326d04
@ -144,9 +144,9 @@ public class MainActivity extends BaseActivity {
|
|||||||
|
|
||||||
//binding.fabMenu.setOnClickListener(view -> NavigationUtil.navigate(this, MenuFragment.class, null)); TODO: remove old menu
|
//binding.fabMenu.setOnClickListener(view -> NavigationUtil.navigate(this, MenuFragment.class, null)); TODO: remove old menu
|
||||||
binding.fabMenu.setOnClickListener(view -> NavigationUtil.openMenu(this, null));
|
binding.fabMenu.setOnClickListener(view -> NavigationUtil.openMenu(this, null));
|
||||||
binding.fabScan.setOnClickListener(view -> scanCode());
|
binding.fabScan.setOnClickListener(view -> scanCode(null));
|
||||||
binding.fabScanImage.setOnClickListener(view -> scanCodeFromImage());
|
binding.fabScanImage.setOnClickListener(view -> scanCodeFromImage(null));
|
||||||
binding.fabInput.setOnClickListener(view -> inputCode());
|
binding.fabInput.setOnClickListener(view -> inputCode(null));
|
||||||
|
|
||||||
Fragment fragment = NavigationUtil.getCurrentFragment(this);
|
Fragment fragment = NavigationUtil.getCurrentFragment(this);
|
||||||
if(fragment instanceof NamedFragment) {
|
if(fragment instanceof NamedFragment) {
|
||||||
@ -210,18 +210,18 @@ public class MainActivity extends BaseActivity {
|
|||||||
NavigationUtil.navigate(this, AboutFragment.class, null);
|
NavigationUtil.navigate(this, AboutFragment.class, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scanCode() {
|
public void scanCode(MenuItem item) {
|
||||||
lockOnPause = false;
|
lockOnPause = false;
|
||||||
startQRCodeScan.launch(null);
|
startQRCodeScan.launch(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scanCodeFromImage() {
|
public void scanCodeFromImage(MenuItem item) {
|
||||||
pickQRCodeImage.launch(new PickVisualMediaRequest.Builder()
|
pickQRCodeImage.launch(new PickVisualMediaRequest.Builder()
|
||||||
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE)
|
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void inputCode() {
|
public void inputCode(MenuItem item) {
|
||||||
DialogInputCodeChoiceBinding binding = DialogInputCodeChoiceBinding.inflate(getLayoutInflater());
|
DialogInputCodeChoiceBinding binding = DialogInputCodeChoiceBinding.inflate(getLayoutInflater());
|
||||||
|
|
||||||
String[] options = new String[2];
|
String[] options = new String[2];
|
||||||
|
@ -63,7 +63,7 @@ public class GroupFragment extends NamedFragment {
|
|||||||
|
|
||||||
groupID = requireArguments().getString(GroupFragment.BUNDLE_GROUP);
|
groupID = requireArguments().getString(GroupFragment.BUNDLE_GROUP);
|
||||||
|
|
||||||
FabUtil.showFabs(requireActivity());
|
//FabUtil.showFabs(requireActivity());
|
||||||
|
|
||||||
otpListAdapter = new OTPListAdapter(requireContext(), binding.itemList);
|
otpListAdapter = new OTPListAdapter(requireContext(), binding.itemList);
|
||||||
binding.itemList.setAdapter(otpListAdapter);
|
binding.itemList.setAdapter(otpListAdapter);
|
||||||
|
@ -9,12 +9,16 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.cringe_studios.cringe_authenticator.R;
|
import com.cringe_studios.cringe_authenticator.R;
|
||||||
|
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
|
||||||
import com.cringe_studios.cringe_authenticator.databinding.FragmentMenuDrawerBinding;
|
import com.cringe_studios.cringe_authenticator.databinding.FragmentMenuDrawerBinding;
|
||||||
import com.cringe_studios.cringe_authenticator.grouplist.GroupListAdapter;
|
import com.cringe_studios.cringe_authenticator.grouplist.GroupListAdapter;
|
||||||
import com.cringe_studios.cringe_authenticator.grouplist.GroupListItem;
|
import com.cringe_studios.cringe_authenticator.grouplist.GroupListItem;
|
||||||
|
import com.cringe_studios.cringe_authenticator.model.OTPData;
|
||||||
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
|
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
|
||||||
import com.cringe_studios.cringe_authenticator.util.FabUtil;
|
import com.cringe_studios.cringe_authenticator.util.FabUtil;
|
||||||
import com.cringe_studios.cringe_authenticator.util.NavigationUtil;
|
import com.cringe_studios.cringe_authenticator.util.NavigationUtil;
|
||||||
|
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.cringe_studios.cringe_authenticator.util.SettingsUtil;
|
||||||
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
|
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
@ -104,8 +108,16 @@ public class MenuDrawerFragment extends BottomSheetDialogFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void removeGroup(String group) {
|
public void removeGroup(String group) {
|
||||||
SettingsUtil.removeGroup(requireContext(), group);
|
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
|
||||||
groupListAdapter.remove(group);
|
try {
|
||||||
|
OTPDatabase.getLoadedDatabase().removeOTPs(group);
|
||||||
|
OTPDatabase.saveDatabase(requireContext(), SettingsUtil.getCryptoParameters(requireContext()));
|
||||||
|
SettingsUtil.removeGroup(requireContext(), group);
|
||||||
|
groupListAdapter.remove(group);
|
||||||
|
} catch (OTPDatabaseException | CryptoException e) {
|
||||||
|
DialogUtil.showErrorDialog(requireContext(), e.toString());
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void renameGroup(String group, String newName) {
|
public void renameGroup(String group, String newName) {
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package com.cringe_studios.cringe_authenticator.util;
|
||||||
|
|
||||||
|
public class BackupException extends Exception {
|
||||||
|
public BackupException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public BackupException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BackupException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BackupException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package com.cringe_studios.cringe_authenticator.util;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
|
||||||
|
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.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import org.bouncycastle.jcajce.provider.symmetric.ARC4;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
|
public class BackupUtil {
|
||||||
|
|
||||||
|
public static void saveBackup(File backupFile, SecretKey key, CryptoParameters parameters) throws BackupException, CryptoException {
|
||||||
|
if(!OTPDatabase.isDatabaseLoaded()) throw new BackupException("Database is not loaded");
|
||||||
|
|
||||||
|
if(!backupFile.exists()) {
|
||||||
|
File parent = backupFile.getParentFile();
|
||||||
|
if(parent != null && !parent.exists()) parent.mkdirs();
|
||||||
|
try {
|
||||||
|
backupFile.createNewFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BackupException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
try(FileOutputStream fOut = new FileOutputStream(backupFile)) {
|
||||||
|
fOut.write(object.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BackupException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CryptoParameters loadParametersFromBackup(File backupFile) throws BackupException {
|
||||||
|
try {
|
||||||
|
byte[] backupBytes = IOUtil.readBytes(backupFile);
|
||||||
|
JsonObject object = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), JsonObject.class);
|
||||||
|
return SettingsUtil.GSON.fromJson(object.get("parameters"), CryptoParameters.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BackupException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OTPDatabase loadBackup(File backupFile, SecretKey key, CryptoParameters parameters) throws BackupException, OTPDatabaseException, CryptoException {
|
||||||
|
try {
|
||||||
|
byte[] backupBytes = IOUtil.readBytes(backupFile);
|
||||||
|
JsonObject object = SettingsUtil.GSON.fromJson(new String(backupBytes, StandardCharsets.UTF_8), JsonObject.class);
|
||||||
|
return OTPDatabase.loadFromEncryptedBytes(Base64.decode(object.get("database").getAsString(), Base64.DEFAULT), key, parameters);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BackupException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package com.cringe_studios.cringe_authenticator.util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
public class IOUtil {
|
||||||
|
|
||||||
|
public static byte[] readBytes(File file) throws IOException {
|
||||||
|
try(FileInputStream fIn = new FileInputStream(file)) {
|
||||||
|
ByteBuffer fileBuffer = ByteBuffer.allocate((int) file.length());
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
while ((len = fIn.read(buffer)) > 0) {
|
||||||
|
fileBuffer.put(buffer, 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileBuffer.array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeBytes(File file, byte[] bytes) throws IOException {
|
||||||
|
try(FileOutputStream fOut = new FileOutputStream(file)) {
|
||||||
|
fOut.write(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -102,13 +102,7 @@ public class OTPDatabase {
|
|||||||
fileBuffer.put(buffer, 0, len);
|
fileBuffer.put(buffer, 0, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(key == null) {
|
loadedDatabase = loadFromEncryptedBytes(fileBuffer.array(), key, SettingsUtil.getCryptoParameters(context));
|
||||||
loadedDatabase = loadFromBytes(fileBuffer.array());
|
|
||||||
loadedKey = null;
|
|
||||||
return loadedDatabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedDatabase = loadFromBytes(Crypto.decrypt(SettingsUtil.getCryptoParameters(context), fileBuffer.array(), key));
|
|
||||||
loadedKey = key;
|
loadedKey = key;
|
||||||
return loadedDatabase;
|
return loadedDatabase;
|
||||||
}catch(IOException e) {
|
}catch(IOException e) {
|
||||||
@ -134,6 +128,14 @@ public class OTPDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static OTPDatabase loadFromEncryptedBytes(byte[] bytes, SecretKey key, CryptoParameters parameters) throws CryptoException, OTPDatabaseException {
|
||||||
|
if(key != null) {
|
||||||
|
bytes = Crypto.decrypt(parameters, bytes, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadFromBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] convertToBytes(OTPDatabase db) {
|
private static byte[] convertToBytes(OTPDatabase db) {
|
||||||
JsonObject object = new JsonObject();
|
JsonObject object = new JsonObject();
|
||||||
for(Map.Entry<String, List<OTPData>> en : db.otps.entrySet()) {
|
for(Map.Entry<String, List<OTPData>> en : db.otps.entrySet()) {
|
||||||
@ -142,16 +144,22 @@ public class OTPDatabase {
|
|||||||
return object.toString().getBytes(StandardCharsets.UTF_8);
|
return object.toString().getBytes(StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void saveDatabase(Context ctx, CryptoParameters parameters) throws OTPDatabaseException, CryptoException {
|
public static byte[] convertToEncryptedBytes(OTPDatabase db, SecretKey key, CryptoParameters parameters) throws CryptoException {
|
||||||
if(!isDatabaseLoaded()) throw new IllegalStateException("Database is not loaded");
|
|
||||||
File file = new File(ctx.getFilesDir(), DB_FILE_NAME);
|
|
||||||
|
|
||||||
byte[] dbBytes = convertToBytes(loadedDatabase);
|
byte[] dbBytes = convertToBytes(loadedDatabase);
|
||||||
|
|
||||||
if(loadedKey != null) {
|
if(key != null) {
|
||||||
dbBytes = Crypto.encrypt(parameters, dbBytes, loadedKey);
|
dbBytes = Crypto.encrypt(parameters, dbBytes, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return dbBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveDatabase(Context ctx, CryptoParameters parameters) throws OTPDatabaseException, CryptoException {
|
||||||
|
if(!isDatabaseLoaded()) throw new OTPDatabaseException("Database is not loaded");
|
||||||
|
File file = new File(ctx.getFilesDir(), DB_FILE_NAME);
|
||||||
|
|
||||||
|
byte[] dbBytes = convertToEncryptedBytes(loadedDatabase, loadedKey, parameters);
|
||||||
|
|
||||||
try(FileOutputStream fOut = new FileOutputStream(file)) {
|
try(FileOutputStream fOut = new FileOutputStream(file)) {
|
||||||
fOut.write(dbBytes);
|
fOut.write(dbBytes);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -172,13 +180,13 @@ public class OTPDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void encrypt(Context ctx, SecretKey key, CryptoParameters parameters) throws OTPDatabaseException, CryptoException {
|
public static void encrypt(Context ctx, SecretKey key, CryptoParameters parameters) throws OTPDatabaseException, CryptoException {
|
||||||
if(!isDatabaseLoaded()) throw new IllegalStateException("Database is not loaded");
|
if(!isDatabaseLoaded()) throw new OTPDatabaseException("Database is not loaded");
|
||||||
loadedKey = key;
|
loadedKey = key;
|
||||||
saveDatabase(ctx, parameters);
|
saveDatabase(ctx, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void decrypt(Context ctx) throws OTPDatabaseException, CryptoException {
|
public static void decrypt(Context ctx) throws OTPDatabaseException, CryptoException {
|
||||||
if(!isDatabaseLoaded()) throw new IllegalStateException("Database is not loaded");
|
if(!isDatabaseLoaded()) throw new OTPDatabaseException("Database is not loaded");
|
||||||
loadedKey = null;
|
loadedKey = null;
|
||||||
saveDatabase(ctx, null);
|
saveDatabase(ctx, null);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/cringeauth_white"
|
app:srcCompat="@drawable/cringeauth_white" />
|
||||||
app:tint="?android:attr/textColor" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -25,8 +25,7 @@
|
|||||||
android:paddingLeft="10dp"
|
android:paddingLeft="10dp"
|
||||||
android:paddingEnd="10dp"
|
android:paddingEnd="10dp"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
app:srcCompat="@drawable/cringeauth_white"
|
app:srcCompat="@drawable/cringeauth_white" />
|
||||||
tools:srcCompat="@drawable/cringeauth_white" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -2,6 +2,12 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context="com.cringe_studios.cringe_authenticator.MainActivity">
|
tools:context="com.cringe_studios.cringe_authenticator.MainActivity">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_lock"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
android:title="Lock"
|
||||||
|
app:showAsAction="never"
|
||||||
|
android:onClick="lockApp" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_settings"
|
android:id="@+id/action_settings"
|
||||||
android:orderInCategory="100"
|
android:orderInCategory="100"
|
||||||
@ -14,10 +20,4 @@
|
|||||||
android:title="@string/action_about"
|
android:title="@string/action_about"
|
||||||
app:showAsAction="never"
|
app:showAsAction="never"
|
||||||
android:onClick="openAbout" />
|
android:onClick="openAbout" />
|
||||||
<item
|
|
||||||
android:id="@+id/action_lock"
|
|
||||||
android:orderInCategory="100"
|
|
||||||
android:title="Lock"
|
|
||||||
app:showAsAction="never"
|
|
||||||
android:onClick="lockApp" />
|
|
||||||
</menu>
|
</menu>
|
@ -6,8 +6,31 @@
|
|||||||
android:orderInCategory="100"
|
android:orderInCategory="100"
|
||||||
android:icon="@drawable/baseline_add_24"
|
android:icon="@drawable/baseline_add_24"
|
||||||
android:title="@string/action_new_group"
|
android:title="@string/action_new_group"
|
||||||
android:onClick="addOTP"
|
app:showAsAction="ifRoom">
|
||||||
app:showAsAction="ifRoom" />
|
<menu>
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_otp_input"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
android:icon="@drawable/baseline_edit_24"
|
||||||
|
android:title="Input manually"
|
||||||
|
android:onClick="inputCode"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_otp_scan"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
android:icon="@drawable/baseline_qr_code_scanner_24"
|
||||||
|
android:title="Scan QR code"
|
||||||
|
android:onClick="scanCode"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_otp_scan_image"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
android:icon="@drawable/baseline_compare_24"
|
||||||
|
android:title="Scan image"
|
||||||
|
android:onClick="scanCodeFromImage"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
</menu>
|
||||||
|
</item>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_settings"
|
android:id="@+id/action_settings"
|
||||||
android:orderInCategory="100"
|
android:orderInCategory="100"
|
||||||
|
Loading…
Reference in New Issue
Block a user