diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/MainActivity.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/MainActivity.java index 86a4887..73b09e8 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/MainActivity.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/MainActivity.java @@ -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.openMenu(this, null)); - binding.fabScan.setOnClickListener(view -> scanCode()); - binding.fabScanImage.setOnClickListener(view -> scanCodeFromImage()); - binding.fabInput.setOnClickListener(view -> inputCode()); + binding.fabScan.setOnClickListener(view -> scanCode(null)); + binding.fabScanImage.setOnClickListener(view -> scanCodeFromImage(null)); + binding.fabInput.setOnClickListener(view -> inputCode(null)); Fragment fragment = NavigationUtil.getCurrentFragment(this); if(fragment instanceof NamedFragment) { @@ -210,18 +210,18 @@ public class MainActivity extends BaseActivity { NavigationUtil.navigate(this, AboutFragment.class, null); } - public void scanCode() { + public void scanCode(MenuItem item) { lockOnPause = false; startQRCodeScan.launch(null); } - public void scanCodeFromImage() { + public void scanCodeFromImage(MenuItem item) { pickQRCodeImage.launch(new PickVisualMediaRequest.Builder() .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE) .build()); } - public void inputCode() { + public void inputCode(MenuItem item) { DialogInputCodeChoiceBinding binding = DialogInputCodeChoiceBinding.inflate(getLayoutInflater()); String[] options = new String[2]; diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/GroupFragment.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/GroupFragment.java index cecbaca..6fc33cc 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/GroupFragment.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/GroupFragment.java @@ -63,7 +63,7 @@ public class GroupFragment extends NamedFragment { groupID = requireArguments().getString(GroupFragment.BUNDLE_GROUP); - FabUtil.showFabs(requireActivity()); + //FabUtil.showFabs(requireActivity()); otpListAdapter = new OTPListAdapter(requireContext(), binding.itemList); binding.itemList.setAdapter(otpListAdapter); diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/MenuDrawerFragment.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/MenuDrawerFragment.java index 7a2f81e..0ab78d9 100644 --- a/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/MenuDrawerFragment.java +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/fragment/MenuDrawerFragment.java @@ -9,12 +9,16 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.grouplist.GroupListAdapter; 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.FabUtil; 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.StyledDialogBuilder; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; @@ -104,8 +108,16 @@ public class MenuDrawerFragment extends BottomSheetDialogFragment { } public void removeGroup(String group) { - SettingsUtil.removeGroup(requireContext(), group); - groupListAdapter.remove(group); + OTPDatabase.promptLoadDatabase(requireActivity(), () -> { + 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) { diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/BackupException.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/BackupException.java new file mode 100644 index 0000000..a61993b --- /dev/null +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/BackupException.java @@ -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); + } +} 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/util/BackupUtil.java new file mode 100644 index 0000000..da0bd1e --- /dev/null +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/BackupUtil.java @@ -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); + } + } + +} diff --git a/app/src/main/java/com/cringe_studios/cringe_authenticator/util/IOUtil.java b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/IOUtil.java new file mode 100644 index 0000000..a74dc7d --- /dev/null +++ b/app/src/main/java/com/cringe_studios/cringe_authenticator/util/IOUtil.java @@ -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); + } + } + +} 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 22e284e..adf5725 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 @@ -102,13 +102,7 @@ public class OTPDatabase { fileBuffer.put(buffer, 0, len); } - if(key == null) { - loadedDatabase = loadFromBytes(fileBuffer.array()); - loadedKey = null; - return loadedDatabase; - } - - loadedDatabase = loadFromBytes(Crypto.decrypt(SettingsUtil.getCryptoParameters(context), fileBuffer.array(), key)); + loadedDatabase = loadFromEncryptedBytes(fileBuffer.array(), key, SettingsUtil.getCryptoParameters(context)); loadedKey = key; return loadedDatabase; }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) { JsonObject object = new JsonObject(); for(Map.Entry> en : db.otps.entrySet()) { @@ -142,16 +144,22 @@ public class OTPDatabase { return object.toString().getBytes(StandardCharsets.UTF_8); } - public static void saveDatabase(Context ctx, CryptoParameters parameters) throws OTPDatabaseException, CryptoException { - if(!isDatabaseLoaded()) throw new IllegalStateException("Database is not loaded"); - File file = new File(ctx.getFilesDir(), DB_FILE_NAME); - + public static byte[] convertToEncryptedBytes(OTPDatabase db, SecretKey key, CryptoParameters parameters) throws CryptoException { byte[] dbBytes = convertToBytes(loadedDatabase); - if(loadedKey != null) { - dbBytes = Crypto.encrypt(parameters, dbBytes, loadedKey); + if(key != null) { + 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)) { fOut.write(dbBytes); } catch (IOException e) { @@ -172,13 +180,13 @@ public class OTPDatabase { } 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; saveDatabase(ctx, parameters); } 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; saveDatabase(ctx, null); } diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 866574d..722c0af 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -17,7 +17,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/cringeauth_white" - app:tint="?android:attr/textColor" /> + app:srcCompat="@drawable/cringeauth_white" /> \ No newline at end of file diff --git a/app/src/main/res/layout/otp_code.xml b/app/src/main/res/layout/otp_code.xml index d5047c3..e1d1c9f 100644 --- a/app/src/main/res/layout/otp_code.xml +++ b/app/src/main/res/layout/otp_code.xml @@ -25,8 +25,7 @@ android:paddingLeft="10dp" android:paddingEnd="10dp" android:scaleType="centerInside" - app:srcCompat="@drawable/cringeauth_white" - tools:srcCompat="@drawable/cringeauth_white" /> + app:srcCompat="@drawable/cringeauth_white" /> + - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_otps.xml b/app/src/main/res/menu/menu_otps.xml index 80d13e6..ee1b5b4 100644 --- a/app/src/main/res/menu/menu_otps.xml +++ b/app/src/main/res/menu/menu_otps.xml @@ -6,8 +6,31 @@ android:orderInCategory="100" android:icon="@drawable/baseline_add_24" android:title="@string/action_new_group" - android:onClick="addOTP" - app:showAsAction="ifRoom" /> + app:showAsAction="ifRoom"> + + + + + +