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

View File

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

View File

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

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);
}
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<String, List<OTPData>> 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);
}

View File

@ -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" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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" />
<LinearLayout
android:layout_width="match_parent"

View File

@ -2,6 +2,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
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
android:id="@+id/action_settings"
android:orderInCategory="100"
@ -14,10 +20,4 @@
android:title="@string/action_about"
app:showAsAction="never"
android:onClick="openAbout" />
<item
android:id="@+id/action_lock"
android:orderInCategory="100"
android:title="Lock"
app:showAsAction="never"
android:onClick="lockApp" />
</menu>

View File

@ -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">
<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
android:id="@+id/action_settings"
android:orderInCategory="100"