diff --git a/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.java b/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.java deleted file mode 100644 index dcee037d..00000000 --- a/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.java +++ /dev/null @@ -1,137 +0,0 @@ -package de.davis.passwordmanager.backup; - -import static de.davis.passwordmanager.utils.BackgroundUtil.doInBackground; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.widget.Toast; - -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.core.os.HandlerCompat; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import java.io.InputStream; -import java.io.OutputStream; - -import javax.crypto.AEADBadTagException; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.dialog.LoadingDialog; - -public abstract class DataBackup { - - - public static final int TYPE_EXPORT = 0; - public static final int TYPE_IMPORT = 1; - - @IntDef({TYPE_EXPORT, TYPE_IMPORT}) - public @interface Type{} - - - private final Context context; - private LoadingDialog loadingDialog; - private final Handler handler = HandlerCompat.createAsync(Looper.getMainLooper()); - - public DataBackup(Context context) { - this.context = context; - } - - public Context getContext() { - return context; - } - - @Nullable - protected abstract Result runExport(OutputStream outputStream) throws Exception; - @Nullable - protected abstract Result runImport(InputStream inputStream) throws Exception; - - public void execute(@Type int type, @Nullable Uri uri){ - execute(type, uri, null); - } - - public void execute(@Type int type, @Nullable Uri uri, OnSyncedHandler onSyncedHandler){ - ContentResolver resolver = getContext().getContentResolver(); - - loadingDialog = new LoadingDialog(getContext()) - .setTitle(type == TYPE_EXPORT ? R.string.export : R.string.import_str) - .setMessage(R.string.wait_text); - AlertDialog alertDialog = loadingDialog.show(); - - doInBackground(() -> { - Result result = null; - try{ - switch (type){ - case TYPE_EXPORT -> result = runExport(resolver.openOutputStream(uri)); - case TYPE_IMPORT -> result = runImport(resolver.openInputStream(uri)); - } - - handleResult(result, onSyncedHandler); - }catch (Exception e){ - e.printStackTrace(); - if(e instanceof NullPointerException) - return; - - error(e); - }finally { - alertDialog.dismiss(); - } - }); - } - - protected void error(Exception exception){ - handler.post(() -> { - String msg = exception.getMessage(); - if(exception instanceof AEADBadTagException) - msg = getContext().getString(R.string.password_does_not_match); - - new MaterialAlertDialogBuilder(getContext()) - .setTitle(R.string.error_title) - .setMessage(msg) - .setPositiveButton(R.string.ok, (dialog, which) -> {}) - .show(); - }); - exception.printStackTrace(); - } - - protected void handleResult(Result result, OnSyncedHandler onSyncedHandler){ - handler.post(() -> { - if(result instanceof Result.Error error) - new MaterialAlertDialogBuilder(getContext()) - .setTitle(R.string.error_title) - .setMessage(error.getMessage()) - .setPositiveButton(R.string.ok, (dialog, which) -> handleSyncHandler(onSyncedHandler, result)) - .show(); - - else if (result instanceof Result.Duplicate duplicate) - new MaterialAlertDialogBuilder(getContext()) - .setTitle(R.string.warning) - .setMessage(getContext().getResources().getQuantityString(R.plurals.item_existed, duplicate.getCount(), duplicate.getCount())) - .setPositiveButton(R.string.ok, (dialog, which) -> handleSyncHandler(onSyncedHandler, result)) - .show(); - - else if (result instanceof Result.Success success) { - Toast.makeText(getContext(), success.getType() == TYPE_EXPORT ? R.string.backup_stored : R.string.backup_restored, Toast.LENGTH_LONG).show(); - handleSyncHandler(onSyncedHandler, result); - } - }); - } - - private void handleSyncHandler(OnSyncedHandler onSyncedHandler, Result result){ - if(onSyncedHandler != null) - onSyncedHandler.onSynced(result); - } - - protected void notifyUpdate(int current, int max){ - handler.post(() -> loadingDialog.updateProgress(current, max)); - } - - public interface OnSyncedHandler { - void onSynced(Result result); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.kt new file mode 100644 index 00000000..95239a54 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.kt @@ -0,0 +1,128 @@ +package de.davis.passwordmanager.backup + +import android.content.Context +import android.content.DialogInterface +import android.net.Uri +import android.widget.Toast +import androidx.annotation.IntDef +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import de.davis.passwordmanager.R +import de.davis.passwordmanager.dialog.LoadingDialog +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.InputStream +import java.io.OutputStream +import javax.crypto.AEADBadTagException + + +const val TYPE_EXPORT = 0 +const val TYPE_IMPORT = 1 + +@IntDef(TYPE_EXPORT, TYPE_IMPORT) +annotation class Type + +abstract class DataBackup(val context: Context) { + + private lateinit var loadingDialog: LoadingDialog + + @Throws(Exception::class) + internal abstract suspend fun runExport(outputStream: OutputStream): Result + + @Throws(Exception::class) + internal abstract suspend fun runImport(inputStream: InputStream): Result + + open suspend fun execute(@Type type: Int, uri: Uri, onSyncedHandler: OnSyncedHandler? = null) { + val resolver = context.contentResolver + loadingDialog = LoadingDialog(context).apply { + setTitle(if (type == TYPE_EXPORT) R.string.export else R.string.import_str) + setMessage(R.string.wait_text) + } + val alertDialog = withContext(Dispatchers.Main) { loadingDialog.show() } + + try { + withContext(Dispatchers.IO) { + val result: Result = when (type) { + TYPE_EXPORT -> resolver.openOutputStream(uri)?.use { runExport(it) }!! + + TYPE_IMPORT -> resolver.openInputStream(uri)?.use { runImport(it) }!! + + else -> Result.Error("Unexpected error occurred") + } + + handleResult(result, onSyncedHandler) + } + } catch (e: Exception) { + if (e is NullPointerException) return + error(e) + } finally { + alertDialog.dismiss() + } + } + + internal suspend fun error(exception: Exception) { + val msg = if (exception is AEADBadTagException) + context.getString(R.string.password_does_not_match) + else + exception.message + + withContext(Dispatchers.Main) { + MaterialAlertDialogBuilder(context).apply { + setTitle(R.string.error_title) + setMessage(msg) + setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> } + }.show() + } + + exception.printStackTrace() + } + + private suspend fun handleResult(result: Result, onSyncedHandler: OnSyncedHandler?) = + withContext(Dispatchers.Main) { + if (result is Result.Success) { + Toast.makeText( + context, + if (result.type == TYPE_EXPORT) R.string.backup_stored else R.string.backup_restored, + Toast.LENGTH_LONG + ).show() + handleSyncHandler(onSyncedHandler, result) + return@withContext + } + + MaterialAlertDialogBuilder(context).apply { + setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + handleSyncHandler( + onSyncedHandler, + result + ) + } + + if (result is Result.Error) { + setTitle(R.string.error_title) + setMessage(result.message) + } else if (result is Result.Duplicate) { + setTitle(R.string.warning) + setMessage( + context.resources.getQuantityString( + R.plurals.item_existed, + result.count, + result.count + ) + ) + } + }.show() + } + + private fun handleSyncHandler(onSyncedHandler: OnSyncedHandler?, result: Result) { + onSyncedHandler?.onSynced(result) + } + + internal suspend fun notifyUpdate(current: Int, max: Int) { + withContext(Dispatchers.Main) { + loadingDialog.updateProgress(current, max) + } + } + + interface OnSyncedHandler { + fun onSynced(result: Result?) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/backup/Result.java b/app/src/main/java/de/davis/passwordmanager/backup/Result.java deleted file mode 100644 index 793adc48..00000000 --- a/app/src/main/java/de/davis/passwordmanager/backup/Result.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.davis.passwordmanager.backup; - -import static de.davis.passwordmanager.backup.DataBackup.TYPE_IMPORT; - -public class Result { - public static class Success extends Result { - - @DataBackup.Type - private final int type; - - public Success(int type) { - this.type = type; - } - - public int getType() { - return type; - } - } - - public static class Error extends Result { - - private final String message; - - public Error(String message) { - this.message = message; - } - - public String getMessage() { - return message; - } - } - - public static class Duplicate extends Success { - - private final int count; - - public Duplicate(int count) { - super(TYPE_IMPORT); - this.count = count; - } - - public int getCount() { - return count; - } - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/backup/Result.kt b/app/src/main/java/de/davis/passwordmanager/backup/Result.kt new file mode 100644 index 00000000..94d2d102 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/backup/Result.kt @@ -0,0 +1,7 @@ +package de.davis.passwordmanager.backup + +sealed class Result { + open class Success(@field:Type val type: Int) : Result() + class Error(val message: String) : Result() + class Duplicate(val count: Int) : Result() +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.java b/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.java deleted file mode 100644 index 54c6a103..00000000 --- a/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.java +++ /dev/null @@ -1,70 +0,0 @@ -package de.davis.passwordmanager.backup; - -import android.content.Context; -import android.content.DialogInterface; -import android.net.Uri; -import android.text.InputType; -import android.widget.EditText; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.content.res.AppCompatResources; - -import com.google.android.material.textfield.TextInputLayout; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.dialog.EditDialogBuilder; -import de.davis.passwordmanager.ui.views.InformationView; - -public abstract class SecureDataBackup extends DataBackup { - - private String password; - - public SecureDataBackup(Context context) { - super(context); - } - - public String getPassword() { - return password; - } - - private void requestPassword(@Type int type, @Nullable Uri uri, OnSyncedHandler onSyncedHandler){ - InformationView.Information i = new InformationView.Information(); - i.setHint(getContext().getString(R.string.password)); - i.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); - i.setSecret(true); - - AlertDialog alertDialog = new EditDialogBuilder(getContext()) - .setTitle(R.string.password) - .setPositiveButton(R.string.yes, (dialog, which) -> {}) - .withInformation(i) - .withStartIcon(AppCompatResources.getDrawable(getContext(), R.drawable.ic_baseline_password_24)) - .setCancelable(type == TYPE_IMPORT) - .show(); - - /* - Needed for the error message that appears when the password (field) is empty. - otherwise the dialogue would close itself - */ - alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> { - String password = ((EditText)alertDialog.findViewById(R.id.textInputEditText)).getText().toString(); - - if(password.isEmpty()){ - ((TextInputLayout)alertDialog.findViewById(R.id.textInputLayout)) - .setError(getContext().getString(R.string.is_not_filled_in)); - return; - } - - alertDialog.dismiss(); - this.password = password; - - - super.execute(type, uri, onSyncedHandler); - }); - } - - @Override - public void execute(int type, @Nullable Uri uri, OnSyncedHandler onSyncedHandler) { - requestPassword(type, uri, onSyncedHandler); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt new file mode 100644 index 00000000..39e0c348 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt @@ -0,0 +1,72 @@ +package de.davis.passwordmanager.backup + +import android.content.Context +import android.content.DialogInterface +import android.net.Uri +import android.text.InputType +import android.view.View +import android.widget.EditText +import androidx.appcompat.content.res.AppCompatResources +import com.google.android.material.textfield.TextInputLayout +import de.davis.passwordmanager.R +import de.davis.passwordmanager.dialog.EditDialogBuilder +import de.davis.passwordmanager.ui.views.InformationView.Information +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +abstract class SecureDataBackup(context: Context) : DataBackup(context) { + + lateinit var password: String + + private suspend fun requestPassword( + @Type type: Int, + uri: Uri, + onSyncedHandler: OnSyncedHandler? + ) { + val information = Information().apply { + hint = context.getString(R.string.password) + inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD + isSecret = true + } + val alertDialog = withContext(Dispatchers.Main) { + EditDialogBuilder(context).apply { + setTitle(R.string.password) + setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> } + withInformation(information) + withStartIcon( + AppCompatResources.getDrawable( + context, + R.drawable.ic_baseline_password_24 + ) + ) + setCancelable(type == TYPE_IMPORT) + }.show() + } + + /* + Needed for the error message that appears when the password (field) is empty. + otherwise the dialogue would close itself + */ + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { _: View? -> + val password = + alertDialog.findViewById(R.id.textInputEditText)?.text.toString() + if (password.isEmpty()) { + alertDialog.findViewById(R.id.textInputLayout)?.error = + context.getString(R.string.is_not_filled_in) + return@setOnClickListener + } + alertDialog.dismiss() + this.password = password + CoroutineScope(Job() + Dispatchers.IO).launch { + super.execute(type, uri, onSyncedHandler) + } + } + } + + override suspend fun execute(@Type type: Int, uri: Uri, onSyncedHandler: OnSyncedHandler?) { + requestPassword(type, uri, onSyncedHandler) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.java b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.java deleted file mode 100644 index f59ec739..00000000 --- a/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.java +++ /dev/null @@ -1,103 +0,0 @@ -package de.davis.passwordmanager.backup.csv; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import com.opencsv.CSVReader; -import com.opencsv.CSVReaderBuilder; -import com.opencsv.CSVWriter; -import com.opencsv.CSVWriterBuilder; -import com.opencsv.validators.RowFunctionValidator; - -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.util.List; -import java.util.stream.Collectors; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.backup.DataBackup; -import de.davis.passwordmanager.backup.Result; -import de.davis.passwordmanager.database.KeyGoDatabase; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.SecureElementManager; -import de.davis.passwordmanager.security.element.password.PasswordDetails; - -public class CsvBackup extends DataBackup { - - - public CsvBackup(Context context) { - super(context); - } - - @NonNull - @Override - protected Result runImport(InputStream inputStream) throws Exception { - CSVReader csvReader = new CSVReaderBuilder(new InputStreamReader(inputStream)) - .withSkipLines(1) - .withRowValidator(new RowFunctionValidator(s -> s.length == 5, getContext().getString(R.string.csv_row_number_error))) - .withRowValidator(new RowFunctionValidator(s -> s.length == 5, getContext().getString(R.string.csv_row_number_error))) - .build(); - - String[] line; - - List elements = KeyGoDatabase.getInstance().secureElementDao() - .getAllByType(SecureElement.TYPE_PASSWORD) - .blockingGet(); - - int existed = 0; - while ((line = csvReader.readNext()) != null) { - if(line[0].isEmpty() || line[3].isEmpty()) // name and password must not be empty - continue; - - String title = line[0]; - String origin = line[1]; - String username = line[2]; - String pwd = line[3]; - if(elements.stream().anyMatch(element -> element.getTitle().equals(title) - && ((PasswordDetails)element.getDetail()).getPassword().equals(pwd) - && ((PasswordDetails)element.getDetail()).getUsername().equals(username) - && ((PasswordDetails)element.getDetail()).getOrigin().equals(origin))) { - existed++; - continue; - } - - PasswordDetails details = new PasswordDetails(pwd, origin, username); - SecureElementManager.getInstance().createElement(new SecureElement(details, title)); - } - - csvReader.close(); - - if(existed != 0) - return new Result.Duplicate(existed); - - return new Result.Success(TYPE_IMPORT); - } - - @NonNull - @Override - protected Result runExport(OutputStream outputStream) throws Exception { - CSVWriter csvWriter = (CSVWriter) new CSVWriterBuilder(new OutputStreamWriter(outputStream)) - .build(); - - List elements = KeyGoDatabase.getInstance().secureElementDao() - .getAllByType(SecureElement.TYPE_PASSWORD) - .blockingGet(); - - csvWriter.writeNext(new String[]{"name", "url", "username", "password", "note"}); - - - csvWriter.writeAll(elements.stream().map(pwd -> new String[]{pwd.getTitle(), - ((PasswordDetails)pwd.getDetail()).getOrigin(), - ((PasswordDetails)pwd.getDetail()).getUsername(), - ((PasswordDetails)pwd.getDetail()).getPassword(), - null}).collect(Collectors.toList())); - - csvWriter.flush(); - csvWriter.close(); - - return new Result.Success(TYPE_EXPORT); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt new file mode 100644 index 00000000..30b32886 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt @@ -0,0 +1,90 @@ +package de.davis.passwordmanager.backup.csv + +import android.content.Context +import com.opencsv.CSVReaderBuilder +import com.opencsv.CSVWriterBuilder +import com.opencsv.validators.RowFunctionValidator +import de.davis.passwordmanager.R +import de.davis.passwordmanager.backup.DataBackup +import de.davis.passwordmanager.backup.Result +import de.davis.passwordmanager.backup.TYPE_EXPORT +import de.davis.passwordmanager.backup.TYPE_IMPORT +import de.davis.passwordmanager.database.KeyGoDatabase.Companion.instance +import de.davis.passwordmanager.security.element.SecureElement +import de.davis.passwordmanager.security.element.SecureElementManager +import de.davis.passwordmanager.security.element.password.PasswordDetails +import java.io.InputStream +import java.io.InputStreamReader +import java.io.OutputStream +import java.io.OutputStreamWriter + +class CsvBackup(context: Context) : DataBackup(context) { + @Throws(Exception::class) + override suspend fun runImport(inputStream: InputStream): Result { + val csvReader = CSVReaderBuilder(InputStreamReader(inputStream)).apply { + withSkipLines(1) + withRowValidator( + RowFunctionValidator( + { s: Array -> s.size == 5 }, + context.getString(R.string.csv_row_number_error) + ) + ) + withRowValidator( + RowFunctionValidator( + { s: Array -> s.size == 5 }, + context.getString(R.string.csv_row_number_error) + ) + ) + }.build() + + var line: Array + val elements: List = instance.secureElementDao() + .getAllByType(SecureElement.TYPE_PASSWORD) + .blockingGet() + + var existed = 0 + csvReader.use { + while (csvReader.readNext().also { line = it } != null) { + if (line[0].isEmpty() || line[3].isEmpty()) // name and password must not be empty + continue + + val title = line[0] + val origin = line[1] + val username = line[2] + val pwd = line[3] + + if (elements.any { e -> e.title == title && (e.detail as PasswordDetails).let { it.password == pwd && it.username == username && it.origin == origin } }) { + existed++ + continue + } + val details = PasswordDetails(pwd, origin, username) + SecureElementManager.getInstance().createElement(SecureElement(details, title)) + } + } + return if (existed != 0) Result.Duplicate(existed) else Result.Success(TYPE_IMPORT) + } + + @Throws(Exception::class) + override suspend fun runExport(outputStream: OutputStream): Result { + val csvWriter = CSVWriterBuilder(OutputStreamWriter(outputStream)).build() + + val elements: List = instance.secureElementDao() + .getAllByType(SecureElement.TYPE_PASSWORD) + .blockingGet() + + csvWriter.use { + it.writeNext(arrayOf("name", "url", "username", "password", "note")) + it.writeAll(elements.map { pwd: SecureElement -> + arrayOf( + pwd.title, + (pwd.detail as PasswordDetails).origin, + (pwd.detail as PasswordDetails).username, + (pwd.detail as PasswordDetails).password, + null + ) + }) + it.flush() + } + return Result.Success(TYPE_EXPORT) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.java b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.java deleted file mode 100644 index 3181ed1e..00000000 --- a/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.java +++ /dev/null @@ -1,130 +0,0 @@ -package de.davis.passwordmanager.backup.keygo; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.google.gson.reflect.TypeToken; - -import org.apache.commons.io.IOUtils; - -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.backup.Result; -import de.davis.passwordmanager.backup.SecureDataBackup; -import de.davis.passwordmanager.database.KeyGoDatabase; -import de.davis.passwordmanager.gson.strategies.ExcludeAnnotationStrategy; -import de.davis.passwordmanager.security.Cryptography; -import de.davis.passwordmanager.security.element.ElementDetail; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.SecureElementDetail; -import de.davis.passwordmanager.security.element.SecureElementManager; -import de.davis.passwordmanager.security.element.password.PasswordDetails; - -public class KeyGoBackup extends SecureDataBackup { - - public static class ElementDetailTypeAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public ElementDetail deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - int type = json.getAsJsonObject().get("type").getAsInt(); - if(type == SecureElement.TYPE_PASSWORD) { - JsonArray passwordArray = new JsonArray(); - for (byte b : Cryptography.encryptAES(json.getAsJsonObject().get("password").getAsString().getBytes())) { - passwordArray.add(b); - } - json.getAsJsonObject().add("password", passwordArray); - } - return context.deserialize(json, SecureElementDetail.getFor(type).getElementDetailClass()); - } - - @Override - public JsonElement serialize(ElementDetail src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) { - JsonElement jsonObject = context.serialize(src); - if(src instanceof PasswordDetails pwdSrc) - jsonObject.getAsJsonObject().addProperty("password", pwdSrc.getPassword()); - - jsonObject.getAsJsonObject().addProperty("type", src.getType()); - return jsonObject; - } - } - - private final Gson gson; - - public KeyGoBackup(Context context) { - super(context); - this.gson = new GsonBuilder() - .registerTypeAdapter(ElementDetail.class, new ElementDetailTypeAdapter()) - .setExclusionStrategies(new ExcludeAnnotationStrategy()) - .create(); - } - - @NonNull - @Override - protected Result runImport(InputStream inputStream) throws Exception{ - byte[] file = IOUtils.toByteArray(inputStream); - if(file.length == 0) - return new Result.Error(getContext().getString(R.string.invalid_file_length)); - - file = Cryptography.decryptWithPwd(file, getPassword()); - List list; - try{ - list = gson.fromJson(new String(file), new TypeToken>(){}.getType()); - }catch (Exception e){ - return new Result.Error(getContext().getString(R.string.invalid_file)); - } - - List elements = KeyGoDatabase.getInstance().secureElementDao() - .getAllOnce() - .blockingGet(); - - int existed = 0; - int length = list.size(); - for (int i = 0; i < length; i++) { - SecureElement element = list.get(i); - if(elements.stream().anyMatch(e -> e.getTitle().equals(element.getTitle()) - && e.getDetail().equals(element.getDetail()))) { - existed++; - - notifyUpdate(i+1, length); - continue; - } - - SecureElementManager.getInstance().createElement(element); - notifyUpdate(i+1, length); - } - - if(existed != 0) - return new Result.Duplicate(existed); - - return new Result.Success(TYPE_IMPORT); - } - - @NonNull - @Override - protected Result runExport(OutputStream outputStream) throws Exception { - List elements = KeyGoDatabase.getInstance().secureElementDao() - .getAllOnce() - .blockingGet(); - - String j = gson.toJson(elements); - - outputStream.write(Cryptography.encryptWithPwd(j.getBytes(), getPassword())); - outputStream.close(); - - return new Result.Success(TYPE_EXPORT); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt new file mode 100644 index 00000000..f79e3fb2 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt @@ -0,0 +1,133 @@ +package de.davis.passwordmanager.backup.keygo + +import android.content.Context +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import com.google.gson.reflect.TypeToken +import de.davis.passwordmanager.R +import de.davis.passwordmanager.backup.Result +import de.davis.passwordmanager.backup.SecureDataBackup +import de.davis.passwordmanager.backup.TYPE_EXPORT +import de.davis.passwordmanager.backup.TYPE_IMPORT +import de.davis.passwordmanager.database.KeyGoDatabase.Companion.instance +import de.davis.passwordmanager.gson.strategies.ExcludeAnnotationStrategy +import de.davis.passwordmanager.security.Cryptography +import de.davis.passwordmanager.security.element.ElementDetail +import de.davis.passwordmanager.security.element.SecureElement +import de.davis.passwordmanager.security.element.SecureElementDetail +import de.davis.passwordmanager.security.element.SecureElementManager +import de.davis.passwordmanager.security.element.password.PasswordDetails +import org.apache.commons.io.IOUtils +import java.io.InputStream +import java.io.OutputStream +import java.lang.reflect.Type + +class KeyGoBackup(context: Context) : SecureDataBackup(context) { + + class ElementDetailTypeAdapter : JsonSerializer, + JsonDeserializer { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): ElementDetail { + json.asJsonObject.run { + val type = this["type"].asInt + if (type == SecureElement.TYPE_PASSWORD) { + val passwordArray = JsonArray().apply { + for (b in Cryptography.encryptAES(this@run["password"].asString.toByteArray())) { + add(b) + } + } + + add("password", passwordArray) + } + + return context.deserialize( + json, + SecureElementDetail.getFor(type).elementDetailClass + ) + } + } + + override fun serialize( + src: ElementDetail, + typeOfSrc: Type, + context: JsonSerializationContext + ): JsonElement { + val jsonObject = context.serialize(src) + jsonObject.asJsonObject.run { + if (src is PasswordDetails) addProperty( + "password", + src.password + ) + addProperty("type", src.type) + } + + return jsonObject + } + } + + private val gson = GsonBuilder().apply { + registerTypeAdapter(ElementDetail::class.java, ElementDetailTypeAdapter()) + setExclusionStrategies(ExcludeAnnotationStrategy()) + }.create() + + @Throws(Exception::class) + override suspend fun runImport(inputStream: InputStream): Result { + var file = IOUtils.toByteArray(inputStream) + if (file.isEmpty()) return Result.Error(context.getString(R.string.invalid_file_length)) + + file = Cryptography.decryptWithPwd(file, password) + + val list: List = try { + gson.fromJson( + String(file), + object : TypeToken>() {}.type + ) + } catch (e: Exception) { + return Result.Error(context.getString(R.string.invalid_file)) + } + + val elements = instance.secureElementDao() + .allOnce + .blockingGet() + + var existed = 0 + val length = list.size + + for (i in 0 until length) { + val element = list[i] + if (elements.any { it.title == element.title && it.detail == element.detail }) { + existed++ + notifyUpdate(i + 1, length) + continue + } + SecureElementManager.getInstance().createElement(element) + notifyUpdate(i + 1, length) + } + return if (existed != 0) Result.Duplicate(existed) else Result.Success(TYPE_IMPORT) + } + + @Throws(Exception::class) + override suspend fun runExport(outputStream: OutputStream): Result { + val elements = instance.secureElementDao() + .allOnce + .blockingGet() + + val json = gson.toJson(elements) + + outputStream.use { + it.write(Cryptography.encryptWithPwd(json.toByteArray(), password)) + } + + return Result.Success(TYPE_EXPORT) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java deleted file mode 100644 index 67c8388d..00000000 --- a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java +++ /dev/null @@ -1,137 +0,0 @@ -package de.davis.passwordmanager.ui.backup; - -import android.content.Intent; -import android.os.Bundle; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceFragmentCompat; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import de.davis.passwordmanager.PasswordManagerApplication; -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.backup.DataBackup; -import de.davis.passwordmanager.backup.csv.CsvBackup; -import de.davis.passwordmanager.backup.keygo.KeyGoBackup; -import de.davis.passwordmanager.ui.auth.AuthenticationActivityKt; -import de.davis.passwordmanager.ui.auth.AuthenticationRequest; - -public class BackupFragment extends PreferenceFragmentCompat { - - ActivityResultLauncher auth; - - private static final String TYPE_KEYGO = "keygo"; - private static final String TYPE_CSV = "csv"; - - @Override - public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { - addPreferencesFromResource(R.xml.backup_preferences); - - ActivityResultLauncher csvImportLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> { - CsvBackup backup = new CsvBackup(requireContext()); - backup.execute(DataBackup.TYPE_IMPORT, result); - }); - - ActivityResultLauncher csvExportLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("text/comma-separated-values"), result -> { - if(result == null) - return; - - - CsvBackup backup = new CsvBackup(requireContext()); - backup.execute(DataBackup.TYPE_EXPORT, result); - }); - - ActivityResultLauncher keygoExportLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("application/octet-stream"), result -> { - if(result == null) - return; - - KeyGoBackup backup = new KeyGoBackup(requireContext()); - backup.execute(DataBackup.TYPE_EXPORT, result); - }); - - ActivityResultLauncher keygoImportLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> { - if(result == null) - return; - - KeyGoBackup backup = new KeyGoBackup(requireContext()); - backup.execute(DataBackup.TYPE_IMPORT, result); - }); - - auth = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { - if(result == null || result.getData() == null) - return; - - Bundle data = result.getData().getExtras(); - if(data == null) - return; - - String formatType = data.getString("format_type"); - if(formatType == null) - return; - - switch (data.getInt("type")){ - case DataBackup.TYPE_EXPORT -> { - if(formatType.equals(TYPE_CSV)){ - ((PasswordManagerApplication)requireActivity().getApplication()).disableReAuthentication(); - csvExportLauncher.launch("keygo-passwords.csv"); - }else if(formatType.equals(TYPE_KEYGO)){ - ((PasswordManagerApplication)requireActivity().getApplication()).disableReAuthentication(); - keygoExportLauncher.launch("elements.keygo"); - } - } - case DataBackup.TYPE_IMPORT -> { - if(formatType.equals(TYPE_CSV)){ - ((PasswordManagerApplication)requireActivity().getApplication()).disableReAuthentication(); - csvImportLauncher.launch(new String[]{"text/comma-separated-values"}); - }else if(formatType.equals(TYPE_KEYGO)){ - ((PasswordManagerApplication)requireActivity().getApplication()).disableReAuthentication(); - keygoImportLauncher.launch(new String[]{"application/octet-stream"}); - } - } - } - - - }); - - findPreference(getString(R.string.preference_import_csv)).setOnPreferenceClickListener(preference -> { - launchAuth(DataBackup.TYPE_IMPORT, TYPE_CSV); - return true; - }); - - findPreference(getString(R.string.preference_export_csv)).setOnPreferenceClickListener(preference -> { - new MaterialAlertDialogBuilder(requireContext(), com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered) - .setTitle(R.string.warning) - .setMessage(R.string.csv_export_warning) - .setPositiveButton(R.string.text_continue, - (dialog, which) -> launchAuth(DataBackup.TYPE_EXPORT, TYPE_CSV)) - .setNegativeButton(R.string.use_keygo, (dialog, which) -> launchAuth(DataBackup.TYPE_EXPORT, TYPE_KEYGO)) - .setNeutralButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) - .show(); - return true; - }); - - findPreference(getString(R.string.preference_export_keygo)).setOnPreferenceClickListener(preference -> { - launchAuth(DataBackup.TYPE_EXPORT, TYPE_KEYGO); - return true; - }); - - findPreference(getString(R.string.preference_import_keygo)).setOnPreferenceClickListener(preference -> { - launchAuth(DataBackup.TYPE_IMPORT, TYPE_KEYGO); - return true; - }); - } - - public void launchAuth(@DataBackup.Type int type, String format){ - Bundle bundle = new Bundle(); - bundle.putInt("type", type); - bundle.putString("format_type", format); - auth.launch(AuthenticationActivityKt.createRequestAuthenticationIntent(requireContext(), - new AuthenticationRequest.Builder() - .withMessage(R.string.authenticate_to_proceed) - .withAdditionalExtras(bundle) - .build() - )); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt new file mode 100644 index 00000000..183843e7 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt @@ -0,0 +1,165 @@ +package de.davis.passwordmanager.ui.backup + +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import de.davis.passwordmanager.PasswordManagerApplication +import de.davis.passwordmanager.R +import de.davis.passwordmanager.backup.TYPE_EXPORT +import de.davis.passwordmanager.backup.TYPE_IMPORT +import de.davis.passwordmanager.backup.Type +import de.davis.passwordmanager.backup.csv.CsvBackup +import de.davis.passwordmanager.backup.keygo.KeyGoBackup +import de.davis.passwordmanager.ui.auth.AuthenticationRequest +import de.davis.passwordmanager.ui.auth.createRequestAuthenticationIntent +import kotlinx.coroutines.launch + +private const val TYPE_KEYGO = "keygo" +private const val TYPE_CSV = "csv" + +class BackupFragment : PreferenceFragmentCompat() { + + + private lateinit var csvImportLauncher: ActivityResultLauncher> + private lateinit var csvExportLauncher: ActivityResultLauncher + + private lateinit var keyGoImportLauncher: ActivityResultLauncher> + private lateinit var keyGoExportLauncher: ActivityResultLauncher + + val auth: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult? -> + if (result == null) return@registerForActivityResult + val data = result.data?.extras ?: return@registerForActivityResult + val formatType = data.getString("format_type") ?: return@registerForActivityResult + when (data.getInt("type")) { + TYPE_EXPORT -> { + if (formatType == TYPE_CSV) { + (requireActivity().application as PasswordManagerApplication).disableReAuthentication() + csvExportLauncher.launch("keygo-passwords.csv") + } else if (formatType == TYPE_KEYGO) { + (requireActivity().application as PasswordManagerApplication).disableReAuthentication() + keyGoExportLauncher.launch("elements.keygo") + } + } + + TYPE_IMPORT -> { + if (formatType == TYPE_CSV) { + (requireActivity().application as PasswordManagerApplication).disableReAuthentication() + csvImportLauncher.launch(arrayOf("text/comma-separated-values")) + } else if (formatType == TYPE_KEYGO) { + (requireActivity().application as PasswordManagerApplication).disableReAuthentication() + keyGoImportLauncher.launch(arrayOf("application/octet-stream")) + } + } + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.backup_preferences) + + CsvBackup(requireContext()).run { + csvImportLauncher = + registerForActivityResult, Uri>(ActivityResultContracts.OpenDocument()) { result: Uri? -> + result?.let { + lifecycleScope.launch { + execute(TYPE_IMPORT, result) + } + } + } + + csvExportLauncher = + registerForActivityResult(ActivityResultContracts.CreateDocument("text/comma-separated-values")) { result: Uri? -> + result?.let { + lifecycleScope.launch { + execute(TYPE_EXPORT, result) + } + } + } + } + + KeyGoBackup(requireContext()).run { + keyGoExportLauncher = + registerForActivityResult(ActivityResultContracts.CreateDocument("application/octet-stream")) { result: Uri? -> + result?.let { + lifecycleScope.launch { + execute(TYPE_EXPORT, result) + } + } + } + keyGoImportLauncher = + registerForActivityResult, Uri>(ActivityResultContracts.OpenDocument()) { result: Uri? -> + result?.let { + lifecycleScope.launch { + execute(TYPE_IMPORT, result) + } + } + } + } + + + findPreference(getString(R.string.preference_import_csv))?.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + launchAuth(TYPE_IMPORT, TYPE_CSV) + true + } + + findPreference(getString(R.string.preference_export_csv))?.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + MaterialAlertDialogBuilder( + requireContext(), + com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered + ).apply { + setTitle(R.string.warning) + setMessage(R.string.csv_export_warning) + setPositiveButton(R.string.text_continue) { _: DialogInterface?, _: Int -> + launchAuth( + TYPE_EXPORT, + TYPE_CSV + ) + } + setNegativeButton(R.string.use_keygo) { _: DialogInterface?, _: Int -> + launchAuth( + TYPE_EXPORT, + TYPE_KEYGO + ) + } + setNeutralButton(R.string.cancel) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + }.show() + true + } + + findPreference(getString(R.string.preference_export_keygo))?.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + launchAuth(TYPE_EXPORT, TYPE_KEYGO) + true + } + findPreference(getString(R.string.preference_import_keygo))?.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _: Preference? -> + launchAuth(TYPE_IMPORT, TYPE_KEYGO) + true + } + } + + private fun launchAuth(@Type type: Int, format: String) { + val bundle = Bundle().apply { + putInt("type", type) + putString("format_type", format) + } + auth.launch( + requireContext().createRequestAuthenticationIntent( + AuthenticationRequest.Builder().apply { + withMessage(R.string.authenticate_to_proceed) + withAdditionalExtras(bundle) + }.build() + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.java deleted file mode 100644 index 44d7653b..00000000 --- a/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.davis.passwordmanager.ui.sync; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.appcompat.app.AppCompatActivity; - -import de.davis.passwordmanager.backup.DataBackup; -import de.davis.passwordmanager.backup.keygo.KeyGoBackup; -import de.davis.passwordmanager.databinding.ActivityImportBinding; -import de.davis.passwordmanager.ui.MainActivity; -import de.davis.passwordmanager.ui.auth.AuthenticationActivityKt; -import de.davis.passwordmanager.ui.auth.AuthenticationRequest; - -public class ImportActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ActivityImportBinding binding = ActivityImportBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - ActivityResultLauncher auth = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { - if(result.getResultCode() != RESULT_OK) - return; - - Intent intent = getIntent(); - if(intent == null || intent.getAction() == null) - return; - - if (!intent.getAction().equals(Intent.ACTION_VIEW)) - return; - - Uri fileUri = intent.getData(); - if(fileUri == null) - return; - - KeyGoBackup backup = new KeyGoBackup(this); - backup.execute(DataBackup.TYPE_IMPORT, fileUri, r -> startActivity(new Intent(this, MainActivity.class))); - }); - - Intent authIntent = AuthenticationActivityKt.createRequestAuthenticationIntent(this, AuthenticationRequest.JUST_AUTHENTICATE); - auth.launch(authIntent); - binding.button.setOnClickListener(v -> auth.launch(authIntent)); - } -} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.kt b/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.kt new file mode 100644 index 00000000..d7d7b04e --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.kt @@ -0,0 +1,56 @@ +package de.davis.passwordmanager.ui.sync + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import de.davis.passwordmanager.backup.DataBackup.OnSyncedHandler +import de.davis.passwordmanager.backup.Result +import de.davis.passwordmanager.backup.TYPE_IMPORT +import de.davis.passwordmanager.backup.keygo.KeyGoBackup +import de.davis.passwordmanager.databinding.ActivityImportBinding +import de.davis.passwordmanager.ui.MainActivity +import de.davis.passwordmanager.ui.auth.AuthenticationRequest +import de.davis.passwordmanager.ui.auth.createRequestAuthenticationIntent +import kotlinx.coroutines.launch + +class ImportActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val binding = ActivityImportBinding.inflate( + layoutInflater + ) + setContentView(binding.root) + + val auth = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode != RESULT_OK) return@registerForActivityResult + if (intent.action == null) return@registerForActivityResult + if (intent.action != Intent.ACTION_VIEW) return@registerForActivityResult + val fileUri = intent.data ?: return@registerForActivityResult + + KeyGoBackup(this).run { + lifecycleScope.launch { + execute(TYPE_IMPORT, fileUri, object : OnSyncedHandler { + override fun onSynced(result: Result?) { + startActivity( + Intent( + this@ImportActivity, + MainActivity::class.java + ) + ) + } + }) + } + } + } + val authIntent = createRequestAuthenticationIntent(AuthenticationRequest.JUST_AUTHENTICATE) + auth.run { + launch(authIntent) + binding.button.setOnClickListener { _: View? -> launch(authIntent) } + } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 65c1bbf0..f174348f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,6 @@ buildscript { } } plugins { - id("com.android.application") version "8.1.2" apply false + id("com.android.application") version "8.1.3" apply false id("org.jetbrains.kotlin.android") version "1.9.20" apply false } \ No newline at end of file