Implement backups (WIP), Move add OTP to menu

This commit is contained in:
MrLetsplay 2023-09-24 14:15:25 +02:00
parent ff50e68e90
commit 2045326d04
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
11 changed files with 197 additions and 36 deletions

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"

View File

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

View File

@ -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"