From 8e5046b5cc5d1f06aeabab33c54b94e3f1bbe9b2 Mon Sep 17 00:00:00 2001 From: Davis Date: Fri, 15 Sep 2023 18:07:19 +0200 Subject: [PATCH 01/24] [Sync] Added CSV export and import option Users can now seamlessly import CSV files from their browsers into KeyGo and conveniently export their passwords to CSV files as well. --- app/build.gradle | 2 + .../davis/passwordmanager/sync/Exporter.java | 8 ++ .../davis/passwordmanager/sync/Importer.java | 11 ++ .../de/davis/passwordmanager/sync/Result.java | 20 ++++ .../passwordmanager/sync/csv/CsvExporter.java | 47 ++++++++ .../passwordmanager/sync/csv/CsvImporter.java | 66 +++++++++++ .../ui/backup/BackupFragment.java | 112 ++++++++++++++++++ .../ui/settings/BaseSettingsFragment.java | 12 +- .../baseline_settings_backup_restore_24.xml | 5 + app/src/main/res/navigation/nav_graph.xml | 4 + app/src/main/res/values-de/strings.xml | 18 +++ app/src/main/res/values/preferences.xml | 2 + app/src/main/res/values/strings.xml | 16 +++ app/src/main/res/xml/backup_preferences.xml | 23 ++++ app/src/main/res/xml/root_preferences.xml | 5 + 15 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/de/davis/passwordmanager/sync/Exporter.java create mode 100644 app/src/main/java/de/davis/passwordmanager/sync/Importer.java create mode 100644 app/src/main/java/de/davis/passwordmanager/sync/Result.java create mode 100644 app/src/main/java/de/davis/passwordmanager/sync/csv/CsvExporter.java create mode 100644 app/src/main/java/de/davis/passwordmanager/sync/csv/CsvImporter.java create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java create mode 100644 app/src/main/res/drawable/baseline_settings_backup_restore_24.xml create mode 100644 app/src/main/res/xml/backup_preferences.xml diff --git a/app/build.gradle b/app/build.gradle index f96f9011..d93b0da2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,6 +143,8 @@ dependencies { githubImplementation "com.squareup.retrofit2:retrofit:2.9.0" githubImplementation 'org.kohsuke:github-api:1.314' + implementation 'com.opencsv:opencsv:5.8' + implementation "androidx.room:room-runtime:2.5.2" annotationProcessor "androidx.room:room-compiler:2.5.2" implementation "androidx.room:room-rxjava3:2.5.2" diff --git a/app/src/main/java/de/davis/passwordmanager/sync/Exporter.java b/app/src/main/java/de/davis/passwordmanager/sync/Exporter.java new file mode 100644 index 00000000..9692b4d7 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/sync/Exporter.java @@ -0,0 +1,8 @@ +package de.davis.passwordmanager.sync; + +import de.davis.passwordmanager.sync.Result; + +public interface Exporter { + + Result exportElements() throws Exception; +} diff --git a/app/src/main/java/de/davis/passwordmanager/sync/Importer.java b/app/src/main/java/de/davis/passwordmanager/sync/Importer.java new file mode 100644 index 00000000..e0e8a87c --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/sync/Importer.java @@ -0,0 +1,11 @@ +package de.davis.passwordmanager.sync; + +import androidx.annotation.WorkerThread; + +import de.davis.passwordmanager.sync.Result; + +@WorkerThread +public interface Importer { + + Result importElements() throws Exception; +} diff --git a/app/src/main/java/de/davis/passwordmanager/sync/Result.java b/app/src/main/java/de/davis/passwordmanager/sync/Result.java new file mode 100644 index 00000000..b961d274 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/sync/Result.java @@ -0,0 +1,20 @@ +package de.davis.passwordmanager.sync; + +public class Result { + public static class Success extends Result { + + } + + public static class Error extends Result { + + private final String message; + + public Error(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } +} diff --git a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvExporter.java b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvExporter.java new file mode 100644 index 00000000..c1084fda --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvExporter.java @@ -0,0 +1,47 @@ +package de.davis.passwordmanager.sync.csv; + +import com.opencsv.CSVWriter; +import com.opencsv.CSVWriterBuilder; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.List; +import java.util.stream.Collectors; + +import de.davis.passwordmanager.database.SecureElementDatabase; +import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.security.element.password.PasswordDetails; +import de.davis.passwordmanager.sync.Result; +import de.davis.passwordmanager.sync.Exporter; + +public class CsvExporter implements Exporter { + + private final CSVWriter csvWriter; + + public CsvExporter(OutputStream outputStream) { + this.csvWriter = (CSVWriter) new CSVWriterBuilder(new OutputStreamWriter(outputStream)) + .build(); + } + + @Override + public Result exportElements() throws Exception { + List elements = SecureElementDatabase.getInstance() + .getSecureElementDao() + .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(); + } +} diff --git a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvImporter.java b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvImporter.java new file mode 100644 index 00000000..3501a479 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvImporter.java @@ -0,0 +1,66 @@ +package de.davis.passwordmanager.sync.csv; + +import android.content.Context; + +import com.opencsv.CSVReader; +import com.opencsv.CSVReaderBuilder; +import com.opencsv.validators.RowFunctionValidator; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; + +import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.SecureElementDatabase; +import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.security.element.SecureElementManager; +import de.davis.passwordmanager.security.element.password.PasswordDetails; +import de.davis.passwordmanager.sync.Result; +import de.davis.passwordmanager.sync.Importer; + +public class CsvImporter implements Importer { + + private final CSVReader csvReader; + private final Context context; + + public CsvImporter(InputStream inputStream, Context context) { + csvReader = new CSVReaderBuilder(new InputStreamReader(inputStream)) + .withSkipLines(1) + .withRowValidator(new RowFunctionValidator(s -> s.length == 5, context.getString(R.string.csv_row_number_error))) + .withRowValidator(new RowFunctionValidator(s -> s.length == 5, context.getString(R.string.csv_row_number_error))) + .build(); + this.context = context; + } + + @Override + public Result importElements() throws Exception { + String[] line; + + List elements = SecureElementDatabase.getInstance() + .getSecureElementDao() + .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]; + if(elements.stream().anyMatch(element -> element.getTitle().equals(title))) { + existed++; + continue; + } + + PasswordDetails details = new PasswordDetails(line[3], line[1], line[2]); + SecureElementManager.getInstance().createElement(new SecureElement(details, title)); + } + + csvReader.close(); + + if(existed != 0) + return new Result.Error(context.getResources().getQuantityString(R.plurals.csv_item_title_existed, existed, existed)); + + return new Result.Success(); + } +} 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 new file mode 100644 index 00000000..dde8d7c4 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java @@ -0,0 +1,112 @@ +package de.davis.passwordmanager.ui.backup; + +import static de.davis.passwordmanager.utils.BackgroundUtil.doInBackground; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.Nullable; +import androidx.core.os.HandlerCompat; +import androidx.preference.PreferenceFragmentCompat; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.io.InputStream; +import java.io.OutputStream; + +import de.davis.passwordmanager.R; +import de.davis.passwordmanager.sync.Result; +import de.davis.passwordmanager.sync.csv.CsvExporter; +import de.davis.passwordmanager.sync.csv.CsvImporter; + +public class BackupFragment extends PreferenceFragmentCompat { + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + addPreferencesFromResource(R.xml.backup_preferences); + + ActivityResultLauncher launcher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> { + Handler handler = HandlerCompat.createAsync(Looper.getMainLooper()); + doInBackground(() -> { + try { + try(InputStream in = requireContext().getContentResolver().openInputStream(result)){ + CsvImporter csvImporter = new CsvImporter(in, requireContext()); + Result r = csvImporter.importElements(); + + handleResult(handler, r); + + } + } catch (Exception e) { + error(handler, e); + } + }); + }); + + ActivityResultLauncher launcherChooseDir = registerForActivityResult(new ActivityResultContracts.CreateDocument("text/comma-separated-values"), result -> { + if(result == null) + return; + + Handler handler = HandlerCompat.createAsync(Looper.getMainLooper()); + doInBackground(() -> { + try { + try(OutputStream in = requireContext().getContentResolver().openOutputStream(result)){ + CsvExporter csvImporter = new CsvExporter(in); + Result r = csvImporter.exportElements(); + + handleResult(handler, r); + + } + } catch (Exception e) { + error(handler, e); + } + }); + }); + + findPreference(getString(R.string.preference_import_csv)).setOnPreferenceClickListener(preference -> { + launcher.launch(new String[]{"text/comma-separated-values"}); + return true; + }); + + findPreference(getString(R.string.preference_export_csv)).setOnPreferenceClickListener(preference -> { + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.warning) + .setMessage(R.string.csv_export_warning) + .setPositiveButton(R.string.ok, + (dialog, which) -> { + launcherChooseDir.launch("keygo-passwords.csv"); + dialog.dismiss(); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .show(); + return true; + }); + } + + private void error(Handler handler, Exception e){ + handler.post(() -> new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.error_title) + .setMessage(e.getMessage()) + .setPositiveButton(R.string.ok, + (dialog, which) -> dialog.dismiss()) + .show()); + } + + private void handleResult(Handler handler, Result result){ + handler.post(() -> { + if(result instanceof Result.Error error) + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.error_title) + .setMessage(error.getMessage()) + .setPositiveButton(R.string.ok, + (dialog, which) -> dialog.dismiss()) + .show(); + + else + Toast.makeText(requireContext(), R.string.backup_stored, Toast.LENGTH_LONG).show(); + }); + } +} diff --git a/app/src/main/java/de/davis/passwordmanager/ui/settings/BaseSettingsFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/settings/BaseSettingsFragment.java index 75783c5b..f1fc3d98 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/settings/BaseSettingsFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/settings/BaseSettingsFragment.java @@ -165,7 +165,17 @@ public boolean onPreferenceStartFragment(@NonNull PreferenceFragmentCompat calle return false; NavController navController = NavHostFragment.findNavController(fragment); - navController.navigate(R.id.updaterFragment); + + int id = -1; + switch (Objects.requireNonNull(pref.getFragment())){ + case "de.davis.passwordmanager.ui.settings.VersionFragment" -> id = R.id.updaterFragment; + case "de.davis.passwordmanager.ui.backup.BackupFragment" -> id = R.id.backupFragment; + } + + if(id == -1) + return false; + + navController.navigate(id); return true; } diff --git a/app/src/main/res/drawable/baseline_settings_backup_restore_24.xml b/app/src/main/res/drawable/baseline_settings_backup_restore_24.xml new file mode 100644 index 00000000..c1f3cc27 --- /dev/null +++ b/app/src/main/res/drawable/baseline_settings_backup_restore_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index cae9e1ef..bd0f8ba1 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -48,4 +48,8 @@ android:id="@+id/action_updaterFragment_to_dashboardFragment" app:destination="@id/dashboardFragment" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 11fbefdd..f86b4298 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -116,4 +116,22 @@ Version Fehler aufgetreten + + Achtung! + Backup + Export + CSV Datei + + Backup erfolgreich geladen + Backup erfolgreich gespeichert + Import + Importieren Sie Passwörter von Ihrem Browser. Suchen Sie nach Passwortmanager in den Einstellungen Ihres Browsers und exportienen Sie Ihre Passwörter in einer CSV Datei. + Exportieren Sie Passwörter in ein CSV-Format, um sie z.B in Ihrem Browser zu importieren. + CSV Datei muss 5 Spalten haben. + Bitte beachten Sie, dass dieser Exportprozess keine Verschlüsselung bietet. Wenn nicht autorisierte Dritte Zugriff auf diese Datei erhalten, können sie Ihre Passwörter einsehen. Darüber hinaus ermöglicht diese Option ausschließlich die Exportierung von Passwörtern. + + + %d Element existiert bereits mit dem Titel + %d Elemente existieren bereits mit dem Titel + \ No newline at end of file diff --git a/app/src/main/res/values/preferences.xml b/app/src/main/res/values/preferences.xml index 72c36ad5..cd51ddc9 100644 --- a/app/src/main/res/values/preferences.xml +++ b/app/src/main/res/values/preferences.xml @@ -7,4 +7,6 @@ feature_autofill feature_reauthenticate license + import_csv + export_csv \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 56cb915b..565c916e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -162,4 +162,20 @@ Version Error occurred + + Attention! + Backup + Import + Export + CSV File + Import passwords from your browser. Search for Passwordmanager in your browser\'s Settings and export your passwords as a csv file. + Export passwords in a csv format to import them in your browser\'s password manager + Please note that this export process does not provide encryption, and if unauthorized third parties gain access to this file, they will have visibility into your passwords. Furthermore, please be aware that this option allows the export of passwords exclusively. + CSV file must have 5 rows. + Backup successfully restored + Backup successfully stored + + %d item already existed with the same title + %d items already existed with the same title + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_preferences.xml b/app/src/main/res/xml/backup_preferences.xml new file mode 100644 index 00000000..aa1f243b --- /dev/null +++ b/app/src/main/res/xml/backup_preferences.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 0a565ce4..20906b23 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -22,6 +22,11 @@ android:defaultValue="2" app:updatesContinuously="true"/> + + From 1fd4fa1f1e21dfafaa8e452c6d59c2249e1c5524 Mon Sep 17 00:00:00 2001 From: Davis Date: Thu, 21 Sep 2023 17:07:21 +0200 Subject: [PATCH 02/24] [Sync] Added KeyGo-File export and import option Users can now seamlessly export and import encrypted .keygo files. --- app/src/main/AndroidManifest.xml | 7 + .../database/daos/SecureElementDao.java | 3 + .../security/Cryptography.java | 51 +++++- .../passwordmanager/sync/DataTransfer.java | 104 ++++++++++++ .../davis/passwordmanager/sync/Exporter.java | 8 - .../davis/passwordmanager/sync/Importer.java | 11 -- .../passwordmanager/sync/csv/CsvExporter.java | 47 ------ .../{CsvImporter.java => CsvTransfer.java} | 55 ++++-- .../sync/keygo/KeyGoTransfer.java | 156 ++++++++++++++++++ .../passwordmanager/ui/BaseMainActivity.java | 9 + .../ui/backup/BackupFragment.java | 74 ++++----- .../ui/login/ChangePasswordFragment.java | 7 +- .../ui/login/EnterPasswordFragment.java | 4 +- .../ui/login/LoginActivity.java | 1 + app/src/main/res/values-de/strings.xml | 9 +- app/src/main/res/values/preferences.xml | 2 + app/src/main/res/values/strings.xml | 8 +- app/src/main/res/xml/backup_preferences.xml | 11 ++ 18 files changed, 437 insertions(+), 130 deletions(-) create mode 100644 app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java delete mode 100644 app/src/main/java/de/davis/passwordmanager/sync/Exporter.java delete mode 100644 app/src/main/java/de/davis/passwordmanager/sync/Importer.java delete mode 100644 app/src/main/java/de/davis/passwordmanager/sync/csv/CsvExporter.java rename app/src/main/java/de/davis/passwordmanager/sync/csv/{CsvImporter.java => CsvTransfer.java} (50%) create mode 100644 app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0be39320..8f4ed2ab 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,6 +34,13 @@ + + + + + + + > getAll(); + @Query("SELECT * FROM SecureElement ORDER BY ROWID ASC") + public abstract Single> getAllOnce(); + @Query("SELECT * FROM SecureElement WHERE type = :type ORDER BY ROWID ASC") public abstract Single> getAllByType(@SecureElement.ElementType int type); diff --git a/app/src/main/java/de/davis/passwordmanager/security/Cryptography.java b/app/src/main/java/de/davis/passwordmanager/security/Cryptography.java index bb000bde..4dc6b5bc 100644 --- a/app/src/main/java/de/davis/passwordmanager/security/Cryptography.java +++ b/app/src/main/java/de/davis/passwordmanager/security/Cryptography.java @@ -4,10 +4,17 @@ import java.io.IOException; import java.security.GeneralSecurityException; +import java.security.spec.KeySpec; import java.util.Arrays; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; import at.favre.lib.crypto.bcrypt.BCrypt; import at.favre.lib.crypto.bcrypt.LongPasswordStrategies; @@ -26,18 +33,48 @@ public static boolean checkBcryptHash(String plaintext, byte[] hash){ return BCrypt.verifyer(VERSION_2A, LongPasswordStrategies.hashSha512(VERSION_2A)).verify(plaintext.getBytes(), hash).verified; } + public static byte[] encryptWithPwd(byte[] data, String pwd) throws GeneralSecurityException{ + byte[] salt = new byte[16]; + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(pwd.toCharArray(), salt, 65536, 256); + SecretKey secretKey = factory.generateSecret(spec); + SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES"); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, secret); + + return encryptWithIV(cipher, data); + } + + public static byte[] decryptWithPwd(byte[] data, String pwd) throws GeneralSecurityException{ + byte[] salt = new byte[16]; + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(pwd.toCharArray(), salt, 65536, 256); + SecretKey secretKey = factory.generateSecret(spec); + SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES"); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, secret, new GCMParameterSpec(128, data, 0, IV_SIZE)); + + return cipher.doFinal(data, IV_SIZE, data.length-IV_SIZE); + } + + private static byte[] encryptWithIV(Cipher cipher, byte[] data) throws IllegalBlockSizeException, BadPaddingException { + byte[] iv = cipher.getIV(); + + byte[] encrypted = cipher.doFinal(data); + byte[] encryptedData = Arrays.copyOf(iv, iv.length + encrypted.length ); + System.arraycopy(encrypted, 0, encryptedData, iv.length, encrypted.length); + + return encryptedData; + } + public static byte[] encryptAES(byte[] data) { try { Cipher cipher = KeyUtil.getCipher(); cipher.init(Cipher.ENCRYPT_MODE, KeyUtil.getSecretKey()); - byte[] iv = cipher.getIV(); - - byte[] encrypted = cipher.doFinal(data); - byte[] encryptedData = Arrays.copyOf(iv, iv.length + encrypted.length ); - System.arraycopy(encrypted, 0, encryptedData, iv.length, encrypted.length); - - return encryptedData; + return encryptWithIV(cipher, data); }catch (GeneralSecurityException | IOException e){ e.printStackTrace(); } diff --git a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java new file mode 100644 index 00000000..f9b6c930 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java @@ -0,0 +1,104 @@ +package de.davis.passwordmanager.sync; + +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.WorkerThread; +import androidx.core.os.HandlerCompat; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.crypto.AEADBadTagException; + +import de.davis.passwordmanager.R; + +public abstract class DataTransfer { + + private final Context context; + + public static final int TYPE_EXPORT = 0; + public static final int TYPE_IMPORT = 1; + + @IntDef({TYPE_EXPORT, TYPE_IMPORT}) + public @interface Type{} + + public DataTransfer(Context context) { + this.context = context; + } + + public Context getContext() { + return context; + } + + protected void error(Handler handler, 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) -> dialog.dismiss()) + .show(); + }); + } + + protected void handleResult(Handler handler, Result result){ + 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) -> dialog.dismiss()) + .show(); + + else + Toast.makeText(getContext(), R.string.backup_stored, Toast.LENGTH_LONG).show(); + }); + } + + + @WorkerThread + protected abstract Result importElements(InputStream inputStream, String password) throws Exception; + @WorkerThread + protected abstract Result exportElements(OutputStream outputStream, String password) throws Exception; + + public void start(@Type int type, Uri uri){ + start(type, uri, null); + } + + protected void start(@Type int type, Uri uri, String password) { + ContentResolver resolver = getContext().getContentResolver(); + Handler handler = HandlerCompat.createAsync(Looper.getMainLooper()); + doInBackground(() -> { + Result result = null; + try{ + switch (type){ + case TYPE_EXPORT -> result = exportElements(resolver.openOutputStream(uri), password); + case TYPE_IMPORT -> result = importElements(resolver.openInputStream(uri), password); + } + + if(result == null) + return; + + handleResult(handler, result); + }catch (Exception e){ + error(handler, e); + } + }); + } +} diff --git a/app/src/main/java/de/davis/passwordmanager/sync/Exporter.java b/app/src/main/java/de/davis/passwordmanager/sync/Exporter.java deleted file mode 100644 index 9692b4d7..00000000 --- a/app/src/main/java/de/davis/passwordmanager/sync/Exporter.java +++ /dev/null @@ -1,8 +0,0 @@ -package de.davis.passwordmanager.sync; - -import de.davis.passwordmanager.sync.Result; - -public interface Exporter { - - Result exportElements() throws Exception; -} diff --git a/app/src/main/java/de/davis/passwordmanager/sync/Importer.java b/app/src/main/java/de/davis/passwordmanager/sync/Importer.java deleted file mode 100644 index e0e8a87c..00000000 --- a/app/src/main/java/de/davis/passwordmanager/sync/Importer.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.davis.passwordmanager.sync; - -import androidx.annotation.WorkerThread; - -import de.davis.passwordmanager.sync.Result; - -@WorkerThread -public interface Importer { - - Result importElements() throws Exception; -} diff --git a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvExporter.java b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvExporter.java deleted file mode 100644 index c1084fda..00000000 --- a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvExporter.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.davis.passwordmanager.sync.csv; - -import com.opencsv.CSVWriter; -import com.opencsv.CSVWriterBuilder; - -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.util.List; -import java.util.stream.Collectors; - -import de.davis.passwordmanager.database.SecureElementDatabase; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.password.PasswordDetails; -import de.davis.passwordmanager.sync.Result; -import de.davis.passwordmanager.sync.Exporter; - -public class CsvExporter implements Exporter { - - private final CSVWriter csvWriter; - - public CsvExporter(OutputStream outputStream) { - this.csvWriter = (CSVWriter) new CSVWriterBuilder(new OutputStreamWriter(outputStream)) - .build(); - } - - @Override - public Result exportElements() throws Exception { - List elements = SecureElementDatabase.getInstance() - .getSecureElementDao() - .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(); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvImporter.java b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java similarity index 50% rename from app/src/main/java/de/davis/passwordmanager/sync/csv/CsvImporter.java rename to app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java index 3501a479..677a69d6 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvImporter.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java @@ -4,36 +4,40 @@ 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.database.SecureElementDatabase; import de.davis.passwordmanager.security.element.SecureElement; import de.davis.passwordmanager.security.element.SecureElementManager; import de.davis.passwordmanager.security.element.password.PasswordDetails; +import de.davis.passwordmanager.sync.DataTransfer; import de.davis.passwordmanager.sync.Result; -import de.davis.passwordmanager.sync.Importer; -public class CsvImporter implements Importer { +public class CsvTransfer extends DataTransfer { - private final CSVReader csvReader; - private final Context context; - public CsvImporter(InputStream inputStream, Context context) { - csvReader = new CSVReaderBuilder(new InputStreamReader(inputStream)) - .withSkipLines(1) - .withRowValidator(new RowFunctionValidator(s -> s.length == 5, context.getString(R.string.csv_row_number_error))) - .withRowValidator(new RowFunctionValidator(s -> s.length == 5, context.getString(R.string.csv_row_number_error))) - .build(); - this.context = context; + public CsvTransfer(Context context) { + super(context); } @Override - public Result importElements() throws Exception { + protected Result importElements(InputStream inputStream, String password) 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 = SecureElementDatabase.getInstance() @@ -59,7 +63,32 @@ public Result importElements() throws Exception { csvReader.close(); if(existed != 0) - return new Result.Error(context.getResources().getQuantityString(R.plurals.csv_item_title_existed, existed, existed)); + return new Result.Error(getContext().getResources().getQuantityString(R.plurals.item_title_existed, existed, existed)); + + return new Result.Success(); + } + + @Override + protected Result exportElements(OutputStream outputStream, String password) throws Exception { + CSVWriter csvWriter = (CSVWriter) new CSVWriterBuilder(new OutputStreamWriter(outputStream)) + .build(); + + List elements = SecureElementDatabase.getInstance() + .getSecureElementDao() + .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(); } diff --git a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java new file mode 100644 index 00000000..9c87ff05 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java @@ -0,0 +1,156 @@ +package de.davis.passwordmanager.sync.keygo; + +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.text.InputType; +import android.widget.EditText; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.content.res.AppCompatResources; + +import com.google.android.material.textfield.TextInputLayout; +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.database.SecureElementDatabase; +import de.davis.passwordmanager.dialog.EditDialogBuilder; +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 de.davis.passwordmanager.sync.DataTransfer; +import de.davis.passwordmanager.sync.Result; +import de.davis.passwordmanager.ui.views.InformationView; + +public class KeyGoTransfer extends DataTransfer { + + 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 KeyGoTransfer(Context context) { + super(context); + this.gson = new GsonBuilder().registerTypeAdapter(ElementDetail.class, new ElementDetailTypeAdapter()).create(); + } + + @Override + protected Result importElements(InputStream inputStream, String password) 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, password); + 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 = SecureElementDatabase.getInstance() + .getSecureElementDao() + .getAllOnce() + .blockingGet(); + + int existed = 0; + for (SecureElement element : list) { + if(elements.stream().anyMatch(e -> e.getTitle().equals(element.getTitle()))) { + existed++; + continue; + } + + SecureElementManager.getInstance().createElement(element); + } + + if(existed != 0) + return new Result.Error(getContext().getResources().getQuantityString(R.plurals.item_title_existed, existed, existed)); + + return new Result.Success(); + } + + @Override + protected Result exportElements(OutputStream outputStream, String password) throws Exception { + List elements = SecureElementDatabase.getInstance() + .getSecureElementDao() + .getAllOnce() + .blockingGet(); + + String j = gson.toJson(elements); + + outputStream.write(Cryptography.encryptWithPwd(j.getBytes(), password)); + outputStream.close(); + + return new Result.Success(); + } + + @Override + public void start(int type, Uri uri) { + 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)) + .show(); + + 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; + } + + start(type, uri, password); + alertDialog.dismiss(); + }); + } +} diff --git a/app/src/main/java/de/davis/passwordmanager/ui/BaseMainActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/BaseMainActivity.java index 0a1e788e..a37de5e5 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/BaseMainActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/BaseMainActivity.java @@ -1,5 +1,6 @@ package de.davis.passwordmanager.ui; +import android.net.Uri; import android.os.Bundle; import android.view.View; @@ -12,6 +13,8 @@ import com.google.android.material.navigation.NavigationBarView; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.sync.DataTransfer; +import de.davis.passwordmanager.sync.keygo.KeyGoTransfer; import de.davis.passwordmanager.ui.views.AddBottomSheet; public class BaseMainActivity extends AppCompatActivity { @@ -21,6 +24,12 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + Uri data = getIntent().getData(); + if(data != null){ + KeyGoTransfer transfer = new KeyGoTransfer(this); + transfer.start(DataTransfer.TYPE_IMPORT, data); + } + NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment); if(navHostFragment == null) return; 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 index dde8d7c4..f98e689b 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java @@ -1,27 +1,21 @@ package de.davis.passwordmanager.ui.backup; -import static de.davis.passwordmanager.utils.BackgroundUtil.doInBackground; - import android.os.Bundle; import android.os.Handler; -import android.os.Looper; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; -import androidx.core.os.HandlerCompat; import androidx.preference.PreferenceFragmentCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import java.io.InputStream; -import java.io.OutputStream; - import de.davis.passwordmanager.R; +import de.davis.passwordmanager.sync.DataTransfer; import de.davis.passwordmanager.sync.Result; -import de.davis.passwordmanager.sync.csv.CsvExporter; -import de.davis.passwordmanager.sync.csv.CsvImporter; +import de.davis.passwordmanager.sync.csv.CsvTransfer; +import de.davis.passwordmanager.sync.keygo.KeyGoTransfer; public class BackupFragment extends PreferenceFragmentCompat { @@ -29,45 +23,37 @@ public class BackupFragment extends PreferenceFragmentCompat { public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { addPreferencesFromResource(R.xml.backup_preferences); - ActivityResultLauncher launcher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> { - Handler handler = HandlerCompat.createAsync(Looper.getMainLooper()); - doInBackground(() -> { - try { - try(InputStream in = requireContext().getContentResolver().openInputStream(result)){ - CsvImporter csvImporter = new CsvImporter(in, requireContext()); - Result r = csvImporter.importElements(); - - handleResult(handler, r); - - } - } catch (Exception e) { - error(handler, e); - } - }); + ActivityResultLauncher csvImportLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> { + CsvTransfer transfer = new CsvTransfer(requireContext()); + transfer.start(DataTransfer.TYPE_IMPORT, result); + }); + + ActivityResultLauncher csvExportLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("text/comma-separated-values"), result -> { + if(result == null) + return; + + CsvTransfer transfer = new CsvTransfer(requireContext()); + transfer.start(DataTransfer.TYPE_EXPORT, result); }); - ActivityResultLauncher launcherChooseDir = registerForActivityResult(new ActivityResultContracts.CreateDocument("text/comma-separated-values"), result -> { + ActivityResultLauncher keygoExportLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("application/octet-stream"), result -> { if(result == null) return; - Handler handler = HandlerCompat.createAsync(Looper.getMainLooper()); - doInBackground(() -> { - try { - try(OutputStream in = requireContext().getContentResolver().openOutputStream(result)){ - CsvExporter csvImporter = new CsvExporter(in); - Result r = csvImporter.exportElements(); + KeyGoTransfer transfer = new KeyGoTransfer(requireContext()); + transfer.start(DataTransfer.TYPE_EXPORT, result); + }); - handleResult(handler, r); + ActivityResultLauncher keygoImportLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> { + if(result == null) + return; - } - } catch (Exception e) { - error(handler, e); - } - }); + KeyGoTransfer transfer = new KeyGoTransfer(requireContext()); + transfer.start(DataTransfer.TYPE_IMPORT, result); }); findPreference(getString(R.string.preference_import_csv)).setOnPreferenceClickListener(preference -> { - launcher.launch(new String[]{"text/comma-separated-values"}); + csvImportLauncher.launch(new String[]{"text/comma-separated-values"}); return true; }); @@ -77,13 +63,23 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S .setMessage(R.string.csv_export_warning) .setPositiveButton(R.string.ok, (dialog, which) -> { - launcherChooseDir.launch("keygo-passwords.csv"); + csvExportLauncher.launch("keygo-passwords.csv"); dialog.dismiss(); }) .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) .show(); return true; }); + + findPreference(getString(R.string.preference_export_keygo)).setOnPreferenceClickListener(preference -> { + keygoExportLauncher.launch("elements.keygo"); + return true; + }); + + findPreference(getString(R.string.preference_import_keygo)).setOnPreferenceClickListener(preference -> { + keygoImportLauncher.launch(new String[]{"application/octet-stream"}); + return true; + }); } private void error(Handler handler, Exception e){ diff --git a/app/src/main/java/de/davis/passwordmanager/ui/login/ChangePasswordFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/login/ChangePasswordFragment.java index 2ef0d46e..a7f78fd3 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/login/ChangePasswordFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/login/ChangePasswordFragment.java @@ -18,6 +18,7 @@ import java.util.Objects; +import de.davis.passwordmanager.PasswordManagerApplication; import de.davis.passwordmanager.R; import de.davis.passwordmanager.databinding.FragmentChangePasswordBinding; import de.davis.passwordmanager.security.Authentication; @@ -97,8 +98,10 @@ public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { PreferenceUtil.putBoolean(getContext(), R.string.preference_fingerprint, binding.fingerprint.isChecked()); Toast.makeText(getContext(), R.string.master_password_changed, Toast.LENGTH_LONG).show(); - if(requireActivity().getIntent().getExtras() == null) - startActivity(new Intent(requireContext(), MainActivity.class)); + //if(requireActivity().getIntent().getExtras() == null) + ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false); + startActivity(new Intent(requireContext(), MainActivity.class).setData(requireActivity().getIntent().getData())); + ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(true); } return true; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java index eb7d3302..2cd21e99 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java @@ -9,6 +9,7 @@ import android.content.Intent; import android.os.Bundle; import android.service.autofill.FillRequest; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -59,7 +60,8 @@ public Intent onSuccess() { return null; } - startActivity(new Intent(getContext(), MainActivity.class)); + startActivity(new Intent(getContext(), MainActivity.class) + .setData(requireActivity().getIntent().getData())); return null; } }; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/login/LoginActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/login/LoginActivity.java index 951db4ce..10fd7f0a 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/login/LoginActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/login/LoginActivity.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f86b4298..a0044683 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -121,17 +121,24 @@ Backup Export CSV Datei + KeyGo Datei Backup erfolgreich geladen Backup erfolgreich gespeichert Import + + Importiere Elemente aus einer verschlüsselten .keygo Datei + Exportiere Elemente in eine .keygo Datei. Diese Datei ist durch ein Passwort geschützt + Importieren Sie Passwörter von Ihrem Browser. Suchen Sie nach Passwortmanager in den Einstellungen Ihres Browsers und exportienen Sie Ihre Passwörter in einer CSV Datei. Exportieren Sie Passwörter in ein CSV-Format, um sie z.B in Ihrem Browser zu importieren. CSV Datei muss 5 Spalten haben. Bitte beachten Sie, dass dieser Exportprozess keine Verschlüsselung bietet. Wenn nicht autorisierte Dritte Zugriff auf diese Datei erhalten, können sie Ihre Passwörter einsehen. Darüber hinaus ermöglicht diese Option ausschließlich die Exportierung von Passwörtern. - + %d Element existiert bereits mit dem Titel %d Elemente existieren bereits mit dem Titel + Ungültige Datei Länge + Ungültige Datei \ No newline at end of file diff --git a/app/src/main/res/values/preferences.xml b/app/src/main/res/values/preferences.xml index cd51ddc9..9566c7d1 100644 --- a/app/src/main/res/values/preferences.xml +++ b/app/src/main/res/values/preferences.xml @@ -9,4 +9,6 @@ license import_csv export_csv + import_keygo + export_keygo \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 565c916e..abf3eff7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -168,14 +168,20 @@ Import Export CSV File + KeyGo File + Import elements using a encrypted .keygo file + Export elements using a .keygo file. Elements exported using this option will be securely encrypted with a password + Import passwords from your browser. Search for Passwordmanager in your browser\'s Settings and export your passwords as a csv file. Export passwords in a csv format to import them in your browser\'s password manager Please note that this export process does not provide encryption, and if unauthorized third parties gain access to this file, they will have visibility into your passwords. Furthermore, please be aware that this option allows the export of passwords exclusively. CSV file must have 5 rows. Backup successfully restored Backup successfully stored - + %d item already existed with the same title %d items already existed with the same title + Invalid file length + Invalid file \ No newline at end of file diff --git a/app/src/main/res/xml/backup_preferences.xml b/app/src/main/res/xml/backup_preferences.xml index aa1f243b..2ae881a0 100644 --- a/app/src/main/res/xml/backup_preferences.xml +++ b/app/src/main/res/xml/backup_preferences.xml @@ -4,6 +4,11 @@ + + + + + Date: Fri, 22 Sep 2023 20:35:50 +0200 Subject: [PATCH 03/24] [Sync] Added authentication requirement --- .../passwordmanager/sync/DataTransfer.java | 3 + .../ui/backup/BackupFragment.java | 92 +++++++++++-------- .../ui/login/EnterPasswordFragment.java | 10 ++ 3 files changed, 67 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java index f9b6c930..aef7c069 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java @@ -97,6 +97,9 @@ protected void start(@Type int type, Uri uri, String password) { handleResult(handler, result); }catch (Exception e){ + if(e instanceof NullPointerException) + return; + error(handler, e); } }); 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 index f98e689b..a06a0b2f 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java @@ -1,8 +1,7 @@ package de.davis.passwordmanager.ui.backup; +import android.content.Intent; import android.os.Bundle; -import android.os.Handler; -import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -13,12 +12,14 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.sync.DataTransfer; -import de.davis.passwordmanager.sync.Result; import de.davis.passwordmanager.sync.csv.CsvTransfer; import de.davis.passwordmanager.sync.keygo.KeyGoTransfer; +import de.davis.passwordmanager.ui.login.LoginActivity; public class BackupFragment extends PreferenceFragmentCompat { + ActivityResultLauncher auth; + @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { addPreferencesFromResource(R.xml.backup_preferences); @@ -52,57 +53,72 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S transfer.start(DataTransfer.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 DataTransfer.TYPE_EXPORT -> { + if(formatType.equals("csv")){ + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.warning) + .setMessage(R.string.csv_export_warning) + .setPositiveButton(R.string.ok, + (dialog, which) -> { + csvExportLauncher.launch("keygo-passwords.csv"); + dialog.dismiss(); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .show(); + }else if(formatType.equals("keygo")){ + keygoExportLauncher.launch("elements.keygo"); + } + } + case DataTransfer.TYPE_IMPORT -> { + if(formatType.equals("csv")){ + csvImportLauncher.launch(new String[]{"text/comma-separated-values"}); + }else if(formatType.equals("keygo")){ + keygoImportLauncher.launch(new String[]{"application/octet-stream"}); + } + } + } + + + }); + findPreference(getString(R.string.preference_import_csv)).setOnPreferenceClickListener(preference -> { - csvImportLauncher.launch(new String[]{"text/comma-separated-values"}); + launchAuth(DataTransfer.TYPE_IMPORT, "csv"); return true; }); findPreference(getString(R.string.preference_export_csv)).setOnPreferenceClickListener(preference -> { - new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.warning) - .setMessage(R.string.csv_export_warning) - .setPositiveButton(R.string.ok, - (dialog, which) -> { - csvExportLauncher.launch("keygo-passwords.csv"); - dialog.dismiss(); - }) - .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) - .show(); + launchAuth(DataTransfer.TYPE_EXPORT, "csv"); return true; }); findPreference(getString(R.string.preference_export_keygo)).setOnPreferenceClickListener(preference -> { - keygoExportLauncher.launch("elements.keygo"); + launchAuth(DataTransfer.TYPE_EXPORT, "keygo"); return true; }); findPreference(getString(R.string.preference_import_keygo)).setOnPreferenceClickListener(preference -> { - keygoImportLauncher.launch(new String[]{"application/octet-stream"}); + launchAuth(DataTransfer.TYPE_IMPORT, "keygo"); return true; }); } - private void error(Handler handler, Exception e){ - handler.post(() -> new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.error_title) - .setMessage(e.getMessage()) - .setPositiveButton(R.string.ok, - (dialog, which) -> dialog.dismiss()) - .show()); - } - - private void handleResult(Handler handler, Result result){ - handler.post(() -> { - if(result instanceof Result.Error error) - new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.error_title) - .setMessage(error.getMessage()) - .setPositiveButton(R.string.ok, - (dialog, which) -> dialog.dismiss()) - .show(); - - else - Toast.makeText(requireContext(), R.string.backup_stored, Toast.LENGTH_LONG).show(); - }); + public void launchAuth(@DataTransfer.Type int type, String format){ + Bundle bundle = new Bundle(); + bundle.putInt("type", type); + bundle.putString("format_type", format); + auth.launch(LoginActivity.getIntentForAuthentication(requireContext()).putExtra("data", bundle)); } } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java index 2cd21e99..972f4866 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java @@ -60,6 +60,16 @@ public Intent onSuccess() { return null; } + boolean intentAuthOnly = requireActivity().getIntent().getBooleanExtra(getString(R.string.preference_authenticate_only), false); + if(intentAuthOnly){ + Intent i = new Intent(); + Bundle bundle = requireActivity().getIntent().getBundleExtra("data"); + if(bundle != null) + i.putExtras(bundle); + + return i; + } + startActivity(new Intent(getContext(), MainActivity.class) .setData(requireActivity().getIntent().getData())); return null; From 5d3b74838f163d80486a8cd62230ba2d86193ffe Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 27 Oct 2023 17:38:28 +0200 Subject: [PATCH 04/24] [Sync] Warning before authentication Prior to this change, the application would request user authentication before issuing a security warning when exporting passwords in CSV format. With this update, the order of these actions are reversed, ensuring that users now receive the security warning first, followed by the request for authentication. This change enhances the user experience and prioritizes security awareness. --- .../ui/backup/BackupFragment.java | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) 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 index a06a0b2f..00dcff90 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java @@ -10,6 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.davis.passwordmanager.PasswordManagerApplication; import de.davis.passwordmanager.R; import de.davis.passwordmanager.sync.DataTransfer; import de.davis.passwordmanager.sync.csv.CsvTransfer; @@ -20,6 +21,9 @@ 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); @@ -33,6 +37,7 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S if(result == null) return; + CsvTransfer transfer = new CsvTransfer(requireContext()); transfer.start(DataTransfer.TYPE_EXPORT, result); }); @@ -67,25 +72,18 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S switch (data.getInt("type")){ case DataTransfer.TYPE_EXPORT -> { - if(formatType.equals("csv")){ - new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.warning) - .setMessage(R.string.csv_export_warning) - .setPositiveButton(R.string.ok, - (dialog, which) -> { - csvExportLauncher.launch("keygo-passwords.csv"); - dialog.dismiss(); - }) - .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) - .show(); - }else if(formatType.equals("keygo")){ + if(formatType.equals(TYPE_CSV)){ + csvExportLauncher.launch("keygo-passwords.csv"); + }else if(formatType.equals(TYPE_KEYGO)){ keygoExportLauncher.launch("elements.keygo"); } } case DataTransfer.TYPE_IMPORT -> { - if(formatType.equals("csv")){ + if(formatType.equals(TYPE_CSV)){ + ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false); csvImportLauncher.launch(new String[]{"text/comma-separated-values"}); - }else if(formatType.equals("keygo")){ + }else if(formatType.equals(TYPE_KEYGO)){ + ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false); keygoImportLauncher.launch(new String[]{"application/octet-stream"}); } } @@ -95,22 +93,30 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S }); findPreference(getString(R.string.preference_import_csv)).setOnPreferenceClickListener(preference -> { - launchAuth(DataTransfer.TYPE_IMPORT, "csv"); + launchAuth(DataTransfer.TYPE_IMPORT, TYPE_CSV); return true; }); findPreference(getString(R.string.preference_export_csv)).setOnPreferenceClickListener(preference -> { - launchAuth(DataTransfer.TYPE_EXPORT, "csv"); + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.warning) + .setMessage(R.string.csv_export_warning) + .setPositiveButton(R.string.ok, + (dialog, which) -> { + launchAuth(DataTransfer.TYPE_EXPORT, TYPE_CSV); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .show(); return true; }); findPreference(getString(R.string.preference_export_keygo)).setOnPreferenceClickListener(preference -> { - launchAuth(DataTransfer.TYPE_EXPORT, "keygo"); + launchAuth(DataTransfer.TYPE_EXPORT, TYPE_KEYGO); return true; }); findPreference(getString(R.string.preference_import_keygo)).setOnPreferenceClickListener(preference -> { - launchAuth(DataTransfer.TYPE_IMPORT, "keygo"); + launchAuth(DataTransfer.TYPE_IMPORT, TYPE_KEYGO); return true; }); } From f462a719184dc199ccd4e63741125c0d1e811907 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 27 Oct 2023 17:48:06 +0200 Subject: [PATCH 05/24] [Sync] Updated toast for import This change ensures that users receive an accurate message following a successful import operation. --- .../de/davis/passwordmanager/sync/DataTransfer.java | 5 ++--- .../java/de/davis/passwordmanager/sync/Result.java | 10 ++++++++++ .../de/davis/passwordmanager/sync/csv/CsvTransfer.java | 4 ++-- .../passwordmanager/sync/keygo/KeyGoTransfer.java | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java index aef7c069..d19235c8 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java @@ -15,7 +15,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import java.io.FileNotFoundException; import java.io.InputStream; import java.io.OutputStream; @@ -66,8 +65,8 @@ protected void handleResult(Handler handler, Result result){ (dialog, which) -> dialog.dismiss()) .show(); - else - Toast.makeText(getContext(), R.string.backup_stored, Toast.LENGTH_LONG).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(); }); } diff --git a/app/src/main/java/de/davis/passwordmanager/sync/Result.java b/app/src/main/java/de/davis/passwordmanager/sync/Result.java index b961d274..7ae700f4 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/Result.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/Result.java @@ -3,6 +3,16 @@ public class Result { public static class Success extends Result { + @DataTransfer.Type + private int type; + + public Success(int type) { + this.type = type; + } + + public int getType() { + return type; + } } public static class Error extends Result { diff --git a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java index 677a69d6..4a9adb4c 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java @@ -65,7 +65,7 @@ protected Result importElements(InputStream inputStream, String password) throws if(existed != 0) return new Result.Error(getContext().getResources().getQuantityString(R.plurals.item_title_existed, existed, existed)); - return new Result.Success(); + return new Result.Success(TYPE_IMPORT); } @Override @@ -90,6 +90,6 @@ protected Result exportElements(OutputStream outputStream, String password) thro csvWriter.flush(); csvWriter.close(); - return new Result.Success(); + return new Result.Success(TYPE_EXPORT); } } diff --git a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java index 9c87ff05..1f9c9107 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java @@ -108,7 +108,7 @@ protected Result importElements(InputStream inputStream, String password) throws if(existed != 0) return new Result.Error(getContext().getResources().getQuantityString(R.plurals.item_title_existed, existed, existed)); - return new Result.Success(); + return new Result.Success(TYPE_IMPORT); } @Override @@ -123,7 +123,7 @@ protected Result exportElements(OutputStream outputStream, String password) thro outputStream.write(Cryptography.encryptWithPwd(j.getBytes(), password)); outputStream.close(); - return new Result.Success(); + return new Result.Success(TYPE_EXPORT); } @Override From da1f9d44ad1a2a18df8641d5b433ff4b4f41d4a7 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 27 Oct 2023 17:57:39 +0200 Subject: [PATCH 06/24] [Sync] Non-Cancelable Password Dialog This change ensures that users are required to set a password before proceeding, making the dialog non-cancelable to prevent users from saving an empty file --- .../java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java index 1f9c9107..6ea3cbcc 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java @@ -138,6 +138,7 @@ public void start(int type, Uri uri) { .setPositiveButton(R.string.yes, (dialog, which) -> {}) .withInformation(i) .withStartIcon(AppCompatResources.getDrawable(getContext(), R.drawable.ic_baseline_password_24)) + .setCancelable(false) .show(); alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> { From fff099f5291d670c8a98314267a46861951e6295 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 27 Oct 2023 18:45:47 +0200 Subject: [PATCH 07/24] [Sync] Added backup pref summary --- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/root_preferences.xml | 1 + 3 files changed, 3 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a0044683..bcef6474 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -119,6 +119,7 @@ Achtung! Backup + Exportiere und importiere ein Backup Export CSV Datei KeyGo Datei diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index abf3eff7..5a70cbbd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -165,6 +165,7 @@ Attention! Backup + Export and Import a backup Import Export CSV File diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 20906b23..54709bb1 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -24,6 +24,7 @@ From 03f424188207478d2690b8d5dd64ef998f0fa395 Mon Sep 17 00:00:00 2001 From: Davis Date: Fri, 22 Sep 2023 20:51:13 +0200 Subject: [PATCH 08/24] [Versioning] Fixed build type calculation (cherry picked from commit 5d3fe533419024e088f43774b733fcdf168d5427) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d93b0da2..e0803535 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,7 +28,7 @@ android { def suffixBuild = String.format("%02d", (build % 32) + 1) - switch (build / 32) { + switch ((int) (build / 32)) { case 3: // build >= 96 break // No suffix for release versions case 2: // build >= 64 From ae5d7f03b6c080e262dee7c1d31c13941d18ee4c Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 27 Oct 2023 18:00:23 +0200 Subject: [PATCH 09/24] [Dependencies] Updated outdated dependencies (cherry picked from commit 57d771f15d51a3f98a0b60e91839d8711562c92d) --- app/build.gradle | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e0803535..3b608ac6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -120,7 +120,7 @@ dependencies { } implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.autofill:autofill:1.1.0' @@ -129,11 +129,11 @@ dependencies { implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1' implementation 'com.google.code.gson:gson:2.10.1' implementation 'androidx.recyclerview:recyclerview-selection:1.1.0' - implementation 'androidx.navigation:navigation-fragment:2.6.0' - implementation 'androidx.navigation:navigation-ui:2.6.0' + implementation 'androidx.navigation:navigation-fragment:2.7.4' + implementation 'androidx.navigation:navigation-ui:2.7.4' implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.biometric:biometric:1.1.0' - implementation 'androidx.browser:browser:1.5.0' + implementation 'androidx.browser:browser:1.6.0' implementation 'me.gosimple:nbvcxz:1.5.1' implementation 'at.favre.lib:bcrypt:0.10.2' implementation 'com.github.alvinhkh:TextDrawable:558677ea31' @@ -145,10 +145,10 @@ dependencies { implementation 'com.opencsv:opencsv:5.8' - implementation "androidx.room:room-runtime:2.5.2" - annotationProcessor "androidx.room:room-compiler:2.5.2" - implementation "androidx.room:room-rxjava3:2.5.2" - androidTestImplementation "androidx.room:room-testing:2.5.2" + implementation "androidx.room:room-runtime:2.6.0" + annotationProcessor "androidx.room:room-compiler:2.6.0" + implementation "androidx.room:room-rxjava3:2.6.0" + androidTestImplementation "androidx.room:room-testing:2.6.0" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' From ea76d945d38907dcaa538ec757b4e20e1c623ad3 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 27 Oct 2023 18:13:18 +0200 Subject: [PATCH 10/24] [Internal] Updated AGP to 8.1.2 (cherry picked from commit 238d01f83712bf5c0fdedf827be0cfaac6c2558d) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 73ff9642..993368c8 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { } } plugins { - id 'com.android.application' version '8.1.0' apply false - id 'com.android.library' version '8.1.0' apply false + id 'com.android.application' version '8.1.2' apply false + id 'com.android.library' version '8.1.2' apply false } tasks.register('clean', Delete) { From 7ba7d8c0a9050ecf6c03f2808e1d1d1f0a59c811 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 27 Oct 2023 18:28:01 +0200 Subject: [PATCH 11/24] [Android] Updated targetSdk and compileSdk to 34 (cherry picked from commit 3e86c6aba579f635aa2fd272db44795bc2651a9c) --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3b608ac6..08215548 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ plugins { } android { - compileSdk 33 + compileSdk 34 def versionPropsFile = file("version.properties") def build, major, minor, patch, vCode, vName @@ -49,7 +49,7 @@ android { defaultConfig { applicationId "de.davis.passwordmanager" minSdk 23 - targetSdk 33 + targetSdk 34 versionCode vCode versionName vName From 504104696eaa0f4cfa805e4aece8f80d3b26f5d9 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 27 Oct 2023 19:21:07 +0200 Subject: [PATCH 12/24] [Sync] Changed backup pref summary --- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bcef6474..7fcfb9d2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -119,7 +119,7 @@ Achtung! Backup - Exportiere und importiere ein Backup + Exportiere oder importiere ein Backup Export CSV Datei KeyGo Datei diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5a70cbbd..51559085 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -165,7 +165,7 @@ Attention! Backup - Export and Import a backup + Export or Import a backup Import Export CSV File From b7ddcb05485224063b3e93ae2e8539642591493e Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 27 Oct 2023 20:53:56 +0200 Subject: [PATCH 13/24] [Sync] KEYGO format option added in warning dialog --- .../de/davis/passwordmanager/ui/backup/BackupFragment.java | 7 +++++-- app/src/main/res/values-de/strings.xml | 3 ++- app/src/main/res/values/strings.xml | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) 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 index 00dcff90..3b483767 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java @@ -98,14 +98,17 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S }); findPreference(getString(R.string.preference_export_csv)).setOnPreferenceClickListener(preference -> { - new MaterialAlertDialogBuilder(requireContext()) + 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.ok, (dialog, which) -> { launchAuth(DataTransfer.TYPE_EXPORT, TYPE_CSV); }) - .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .setNegativeButton(R.string.use_keygo, (dialog, which) -> { + launchAuth(DataTransfer.TYPE_EXPORT, TYPE_KEYGO); + }) + .setNeutralButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) .show(); return true; }); diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7fcfb9d2..0d01c690 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -123,6 +123,7 @@ Export CSV Datei KeyGo Datei + KEYGO Format verwenden Backup erfolgreich geladen Backup erfolgreich gespeichert @@ -134,7 +135,7 @@ Importieren Sie Passwörter von Ihrem Browser. Suchen Sie nach Passwortmanager in den Einstellungen Ihres Browsers und exportienen Sie Ihre Passwörter in einer CSV Datei. Exportieren Sie Passwörter in ein CSV-Format, um sie z.B in Ihrem Browser zu importieren. CSV Datei muss 5 Spalten haben. - Bitte beachten Sie, dass dieser Exportprozess keine Verschlüsselung bietet. Wenn nicht autorisierte Dritte Zugriff auf diese Datei erhalten, können sie Ihre Passwörter einsehen. Darüber hinaus ermöglicht diese Option ausschließlich die Exportierung von Passwörtern. + Bitte beachten Sie, dass dieser Exportprozess keine Verschlüsselung bietet. Wenn nicht autorisierte Dritte Zugriff auf diese Datei erhalten, können sie Ihre Passwörter einsehen. Darüber hinaus ermöglicht diese Option ausschließlich die Exportierung von Passwörtern.\n\nEs wird empfohlen, das KEYGO Dateiformat zu wählen! %d Element existiert bereits mit dem Titel diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 51559085..7fe3ab61 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -170,12 +170,13 @@ Export CSV File KeyGo File + Use KEYGO format Import elements using a encrypted .keygo file Export elements using a .keygo file. Elements exported using this option will be securely encrypted with a password Import passwords from your browser. Search for Passwordmanager in your browser\'s Settings and export your passwords as a csv file. Export passwords in a csv format to import them in your browser\'s password manager - Please note that this export process does not provide encryption, and if unauthorized third parties gain access to this file, they will have visibility into your passwords. Furthermore, please be aware that this option allows the export of passwords exclusively. + Please note that this export process does not provide encryption, and if unauthorized third parties gain access to this file, they will have visibility into your passwords. Furthermore, please be aware that this option allows the export of passwords exclusively.\n\nIt is recommended to choose the KEYGO file format! CSV file must have 5 rows. Backup successfully restored Backup successfully stored From c86276810b23845e3c4e6ecd324207998ebb585d Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 27 Oct 2023 21:04:17 +0200 Subject: [PATCH 14/24] [Version] Updated to 1.2.0-beta03 --- app/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.properties b/app/version.properties index 326b39f1..2a33979a 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,6 +1,6 @@ #build: alpha 0-31 | beta 32-63 | rc 64-95 | stable 96-99 #Sat Jul 22 21:39:59 CEST 2023 -build=96 +build=34 major=1 -minor=1 +minor=2 patch=0 From a264dd3fd8206b39b2d45012f4ac12d22ce6cfba Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 28 Oct 2023 13:39:43 +0200 Subject: [PATCH 15/24] [Sync] Fixed Element ID Export/Import Issue This fix addresses the problem where the application would erroneously export and import the ID of an element. This issue previously caused conflicts when attempting to import an element with an existing ID in the database, resulting in the overwriting of database entries. --- .../gson/annotations/Exclude.java | 11 ++++++++ .../strategies/ExcludeAnnotationStrategy.java | 18 +++++++++++++ .../security/element/SecureElement.java | 2 ++ .../element/creditcard/CreditCardDetails.java | 26 +++++++++++++++++++ .../element/password/PasswordDetails.java | 15 +++++++++++ .../passwordmanager/sync/DataTransfer.java | 3 +-- .../sync/keygo/KeyGoTransfer.java | 11 +++++--- 7 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/de/davis/passwordmanager/gson/annotations/Exclude.java create mode 100644 app/src/main/java/de/davis/passwordmanager/gson/strategies/ExcludeAnnotationStrategy.java diff --git a/app/src/main/java/de/davis/passwordmanager/gson/annotations/Exclude.java b/app/src/main/java/de/davis/passwordmanager/gson/annotations/Exclude.java new file mode 100644 index 00000000..7392857e --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/gson/annotations/Exclude.java @@ -0,0 +1,11 @@ +package de.davis.passwordmanager.gson.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Exclude { +} diff --git a/app/src/main/java/de/davis/passwordmanager/gson/strategies/ExcludeAnnotationStrategy.java b/app/src/main/java/de/davis/passwordmanager/gson/strategies/ExcludeAnnotationStrategy.java new file mode 100644 index 00000000..85d0cf25 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/gson/strategies/ExcludeAnnotationStrategy.java @@ -0,0 +1,18 @@ +package de.davis.passwordmanager.gson.strategies; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; + +import de.davis.passwordmanager.gson.annotations.Exclude; + +public class ExcludeAnnotationStrategy implements ExclusionStrategy { + @Override + public boolean shouldSkipField(FieldAttributes f) { + return f.getAnnotation(Exclude.class) != null; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + return false; + } +} diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/SecureElement.java b/app/src/main/java/de/davis/passwordmanager/security/element/SecureElement.java index aabc948e..5f23265e 100644 --- a/app/src/main/java/de/davis/passwordmanager/security/element/SecureElement.java +++ b/app/src/main/java/de/davis/passwordmanager/security/element/SecureElement.java @@ -21,6 +21,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.dashboard.Item; +import de.davis.passwordmanager.gson.annotations.Exclude; @Entity public class SecureElement implements Serializable, Comparable, Item { @@ -45,6 +46,7 @@ public class SecureElement implements Serializable, Comparable, I private boolean favorite; @PrimaryKey(autoGenerate = true) + @Exclude private long id; @ColumnInfo(name = "created_at") diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/creditcard/CreditCardDetails.java b/app/src/main/java/de/davis/passwordmanager/security/element/creditcard/CreditCardDetails.java index 125b05b8..208c7891 100644 --- a/app/src/main/java/de/davis/passwordmanager/security/element/creditcard/CreditCardDetails.java +++ b/app/src/main/java/de/davis/passwordmanager/security/element/creditcard/CreditCardDetails.java @@ -1,5 +1,7 @@ package de.davis.passwordmanager.security.element.creditcard; +import java.util.Objects; + import de.davis.passwordmanager.security.element.ElementDetail; import de.davis.passwordmanager.security.element.SecureElement; import de.davis.passwordmanager.utils.CreditCardUtil; @@ -67,4 +69,28 @@ public void setExpirationDate(String expirationDate) { public int getType() { return SecureElement.TYPE_CREDIT_CARD; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CreditCardDetails that = (CreditCardDetails) o; + + if (!Objects.equals(cardholder, that.cardholder)) + return false; + if (!Objects.equals(expirationDate, that.expirationDate)) + return false; + if (!cardNumber.equals(that.cardNumber)) return false; + return Objects.equals(cvv, that.cvv); + } + + @Override + public int hashCode() { + int result = cardholder != null ? cardholder.hashCode() : 0; + result = 31 * result + (expirationDate != null ? expirationDate.hashCode() : 0); + result = 31 * result + cardNumber.hashCode(); + result = 31 * result + (cvv != null ? cvv.hashCode() : 0); + return result; + } } diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/password/PasswordDetails.java b/app/src/main/java/de/davis/passwordmanager/security/element/password/PasswordDetails.java index 0306590e..5e58d8b7 100644 --- a/app/src/main/java/de/davis/passwordmanager/security/element/password/PasswordDetails.java +++ b/app/src/main/java/de/davis/passwordmanager/security/element/password/PasswordDetails.java @@ -1,5 +1,7 @@ package de.davis.passwordmanager.security.element.password; +import java.util.Objects; + import de.davis.passwordmanager.security.Cryptography; import de.davis.passwordmanager.security.element.ElementDetail; import de.davis.passwordmanager.security.element.SecureElement; @@ -57,4 +59,17 @@ public String getPassword(){ public int getType() { return SecureElement.TYPE_PASSWORD; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PasswordDetails that = (PasswordDetails) o; + return getPassword().equals(that.getPassword()) && Objects.equals(origin, that.origin) && Objects.equals(username, that.username); + } + + @Override + public int hashCode() { + return Objects.hash(origin, username, getPassword()); + } } diff --git a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java index d19235c8..e11d6f89 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java @@ -61,8 +61,7 @@ protected void handleResult(Handler handler, Result result){ new MaterialAlertDialogBuilder(getContext()) .setTitle(R.string.error_title) .setMessage(error.getMessage()) - .setPositiveButton(R.string.ok, - (dialog, which) -> dialog.dismiss()) + .setPositiveButton(R.string.ok, (dialog, which) -> {}) .show(); else if (result instanceof Result.Success success) diff --git a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java index 6ea3cbcc..1bfd1284 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java @@ -31,6 +31,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.SecureElementDatabase; import de.davis.passwordmanager.dialog.EditDialogBuilder; +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; @@ -73,7 +74,10 @@ public JsonElement serialize(ElementDetail src, java.lang.reflect.Type typeOfSrc public KeyGoTransfer(Context context) { super(context); - this.gson = new GsonBuilder().registerTypeAdapter(ElementDetail.class, new ElementDetailTypeAdapter()).create(); + this.gson = new GsonBuilder() + .registerTypeAdapter(ElementDetail.class, new ElementDetailTypeAdapter()) + .setExclusionStrategies(new ExcludeAnnotationStrategy()) + .create(); } @Override @@ -97,7 +101,8 @@ protected Result importElements(InputStream inputStream, String password) throws int existed = 0; for (SecureElement element : list) { - if(elements.stream().anyMatch(e -> e.getTitle().equals(element.getTitle()))) { + if(elements.stream().anyMatch(e -> e.getTitle().equals(element.getTitle()) + && e.getDetail().equals(element.getDetail()))) { existed++; continue; } @@ -142,6 +147,7 @@ public void start(int type, Uri uri) { .show(); alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> { + alertDialog.dismiss(); String password = ((EditText)alertDialog.findViewById(R.id.textInputEditText)).getText().toString(); if(password.isEmpty()){ @@ -151,7 +157,6 @@ public void start(int type, Uri uri) { } start(type, uri, password); - alertDialog.dismiss(); }); } } From 0f5552d2def53ccdb256dcb7bef82d8d53798383 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 28 Oct 2023 16:47:59 +0200 Subject: [PATCH 16/24] [Sync] Added Loading dialog --- .../passwordmanager/dialog/LoadingDialog.java | 31 +++++++++++++++++++ .../passwordmanager/sync/DataTransfer.java | 27 +++++++++++++--- .../sync/keygo/KeyGoTransfer.java | 7 ++++- app/src/main/res/layout/loading_layout.xml | 14 +++++++++ app/src/main/res/values-de/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ 6 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/de/davis/passwordmanager/dialog/LoadingDialog.java create mode 100644 app/src/main/res/layout/loading_layout.xml diff --git a/app/src/main/java/de/davis/passwordmanager/dialog/LoadingDialog.java b/app/src/main/java/de/davis/passwordmanager/dialog/LoadingDialog.java new file mode 100644 index 00000000..ec5f888b --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/dialog/LoadingDialog.java @@ -0,0 +1,31 @@ +package de.davis.passwordmanager.dialog; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.annotation.NonNull; + +import de.davis.passwordmanager.databinding.LoadingLayoutBinding; + +public class LoadingDialog extends BaseDialogBuilder { + + private LoadingLayoutBinding binding; + + public LoadingDialog(@NonNull Context context) { + super(context); + setCancelable(false); + } + + public void updateProgress(int current, int max){ + double progress = (current * 100d / max); + binding.progress.setIndeterminate(false); + binding.progress.setProgressCompat((int) progress, true); + } + + @Override + public View onCreateView(LayoutInflater inflater) { + binding = LoadingLayoutBinding.inflate(inflater); + return binding.getRoot(); + } +} diff --git a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java index e11d6f89..b463493d 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java @@ -11,6 +11,7 @@ import androidx.annotation.IntDef; import androidx.annotation.WorkerThread; +import androidx.appcompat.app.AlertDialog; import androidx.core.os.HandlerCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -21,6 +22,7 @@ import javax.crypto.AEADBadTagException; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.dialog.LoadingDialog; public abstract class DataTransfer { @@ -32,6 +34,10 @@ public abstract class DataTransfer { @IntDef({TYPE_EXPORT, TYPE_IMPORT}) public @interface Type{} + private LoadingDialog loadingDialog; + + private final Handler handler = HandlerCompat.createAsync(Looper.getMainLooper()); + public DataTransfer(Context context) { this.context = context; } @@ -40,7 +46,7 @@ public Context getContext() { return context; } - protected void error(Handler handler, Exception exception){ + protected void error(Exception exception){ handler.post(() -> { String msg = exception.getMessage(); if(exception instanceof AEADBadTagException) @@ -55,7 +61,7 @@ protected void error(Handler handler, Exception exception){ }); } - protected void handleResult(Handler handler, Result result){ + protected void handleResult(Result result){ handler.post(() -> { if(result instanceof Result.Error error) new MaterialAlertDialogBuilder(getContext()) @@ -79,9 +85,18 @@ public void start(@Type int type, Uri uri){ start(type, uri, null); } + protected void notifyUpdate(int current, int max){ + handler.post(() -> loadingDialog.updateProgress(current, max)); + } + protected void start(@Type int type, Uri uri, String password) { ContentResolver resolver = getContext().getContentResolver(); - Handler handler = HandlerCompat.createAsync(Looper.getMainLooper()); + + 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{ @@ -93,12 +108,14 @@ protected void start(@Type int type, Uri uri, String password) { if(result == null) return; - handleResult(handler, result); + handleResult(result); }catch (Exception e){ if(e instanceof NullPointerException) return; - error(handler, e); + error(e); + }finally { + alertDialog.dismiss(); } }); } diff --git a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java index 1bfd1284..f56aaab6 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java @@ -100,14 +100,19 @@ protected Result importElements(InputStream inputStream, String password) throws .blockingGet(); int existed = 0; - for (SecureElement element : list) { + 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) diff --git a/app/src/main/res/layout/loading_layout.xml b/app/src/main/res/layout/loading_layout.xml new file mode 100644 index 00000000..496e67ac --- /dev/null +++ b/app/src/main/res/layout/loading_layout.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0d01c690..0d0256ab 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -143,4 +143,6 @@ Ungültige Datei Länge Ungültige Datei + + Das könnte ein wenig Zeit in Anspruch nehmen. Bitte haben Sie Geduld \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7fe3ab61..05e85eba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -186,4 +186,6 @@ Invalid file length Invalid file + + This might take a little time. Please be patient \ No newline at end of file From 8b0fe6d9362d88aacdcc4b937e337d4f869bb15d Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 28 Oct 2023 16:50:00 +0200 Subject: [PATCH 17/24] [Sync] Improved CSV import duplicate detection --- .../de/davis/passwordmanager/sync/csv/CsvTransfer.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java index 4a9adb4c..7f3af6c7 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java @@ -51,12 +51,18 @@ protected Result importElements(InputStream inputStream, String password) throws continue; String title = line[0]; - if(elements.stream().anyMatch(element -> element.getTitle().equals(title))) { + 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(line[3], line[1], line[2]); + PasswordDetails details = new PasswordDetails(pwd, origin, username); SecureElementManager.getInstance().createElement(new SecureElement(details, title)); } From 219ccdcc1476d19ce257a3e8ebd943d25485dd53 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 28 Oct 2023 16:51:22 +0200 Subject: [PATCH 18/24] [Sync] Improved duplicate skipped dialog message --- .../davis/passwordmanager/sync/DataTransfer.java | 7 +++++++ .../de/davis/passwordmanager/sync/Result.java | 16 ++++++++++++++++ .../passwordmanager/sync/csv/CsvTransfer.java | 2 +- .../sync/keygo/KeyGoTransfer.java | 2 +- app/src/main/res/values-de/strings.xml | 6 +++--- app/src/main/res/values/strings.xml | 6 +++--- 6 files changed, 31 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java index b463493d..3fbdc8a1 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java @@ -70,6 +70,13 @@ protected void handleResult(Result result){ .setPositiveButton(R.string.ok, (dialog, which) -> {}) .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) -> {}) + .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(); }); diff --git a/app/src/main/java/de/davis/passwordmanager/sync/Result.java b/app/src/main/java/de/davis/passwordmanager/sync/Result.java index 7ae700f4..f5de5e12 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/Result.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/Result.java @@ -1,5 +1,7 @@ package de.davis.passwordmanager.sync; +import static de.davis.passwordmanager.sync.DataTransfer.TYPE_IMPORT; + public class Result { public static class Success extends Result { @@ -27,4 +29,18 @@ 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/sync/csv/CsvTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java index 7f3af6c7..4beb29b6 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java @@ -69,7 +69,7 @@ protected Result importElements(InputStream inputStream, String password) throws csvReader.close(); if(existed != 0) - return new Result.Error(getContext().getResources().getQuantityString(R.plurals.item_title_existed, existed, existed)); + return new Result.Duplicate(existed); return new Result.Success(TYPE_IMPORT); } diff --git a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java index f56aaab6..174ecbce 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java @@ -116,7 +116,7 @@ protected Result importElements(InputStream inputStream, String password) throws } if(existed != 0) - return new Result.Error(getContext().getResources().getQuantityString(R.plurals.item_title_existed, existed, existed)); + return new Result.Duplicate(existed); return new Result.Success(TYPE_IMPORT); } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0d0256ab..495b37a7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -137,9 +137,9 @@ CSV Datei muss 5 Spalten haben. Bitte beachten Sie, dass dieser Exportprozess keine Verschlüsselung bietet. Wenn nicht autorisierte Dritte Zugriff auf diese Datei erhalten, können sie Ihre Passwörter einsehen. Darüber hinaus ermöglicht diese Option ausschließlich die Exportierung von Passwörtern.\n\nEs wird empfohlen, das KEYGO Dateiformat zu wählen! - - %d Element existiert bereits mit dem Titel - %d Elemente existieren bereits mit dem Titel + + %d Element wurde ignoriert, da es bereits existiert + %d Elemente wurden ignoriert, da diese bereits existieren Ungültige Datei Länge Ungültige Datei diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05e85eba..2d2ee1cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -180,9 +180,9 @@ CSV file must have 5 rows. Backup successfully restored Backup successfully stored - - %d item already existed with the same title - %d items already existed with the same title + + %d item was skipped as it already existed + %d items were skipped as they already existed Invalid file length Invalid file From 4420859895702ee44ebbf6beec85e24e8e9641e9 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 28 Oct 2023 16:52:12 +0200 Subject: [PATCH 19/24] [Sync] Cancelable import password dialog --- .../java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java index 174ecbce..48026c8b 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java @@ -148,7 +148,7 @@ public void start(int type, Uri uri) { .setPositiveButton(R.string.yes, (dialog, which) -> {}) .withInformation(i) .withStartIcon(AppCompatResources.getDrawable(getContext(), R.drawable.ic_baseline_password_24)) - .setCancelable(false) + .setCancelable(type == TYPE_IMPORT) .show(); alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> { From 88b76d72317d1d7d36c9bd6960a7a5deab32163f Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 28 Oct 2023 16:52:48 +0200 Subject: [PATCH 20/24] [Code] Reformatted code --- app/src/main/AndroidManifest.xml | 35 ++++++++++++------- .../sync/keygo/KeyGoTransfer.java | 28 +++++++-------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8f4ed2ab..aab73fe2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,31 +3,37 @@ xmlns:tools="http://schemas.android.com/tools"> - + - - + + + tools:targetApi="34"> - - - + android:theme="@style/AppTheme.NoActionBar" + android:windowSoftInputMode="adjustNothing" /> + + + @@ -36,7 +42,9 @@ + + @@ -51,6 +59,7 @@ + diff --git a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java index 48026c8b..ab96f27a 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java @@ -1,7 +1,6 @@ package de.davis.passwordmanager.sync.keygo; import android.content.Context; -import android.content.DialogInterface; import android.net.Uri; import android.text.InputType; import android.widget.EditText; @@ -143,25 +142,22 @@ public void start(int type, Uri uri) { i.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); i.setSecret(true); - AlertDialog alertDialog = new EditDialogBuilder(getContext()) + new EditDialogBuilder(getContext()) .setTitle(R.string.password) - .setPositiveButton(R.string.yes, (dialog, which) -> {}) + .setPositiveButton(R.string.yes, (dialog, which) -> { + String password = ((EditText)((AlertDialog)dialog).findViewById(R.id.textInputEditText)).getText().toString(); + + if(password.isEmpty()){ + ((TextInputLayout)((AlertDialog)dialog).findViewById(R.id.textInputLayout)) + .setError(getContext().getString(R.string.is_not_filled_in)); + return; + } + + start(type, uri, password); + }) .withInformation(i) .withStartIcon(AppCompatResources.getDrawable(getContext(), R.drawable.ic_baseline_password_24)) .setCancelable(type == TYPE_IMPORT) .show(); - - alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> { - alertDialog.dismiss(); - 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; - } - - start(type, uri, password); - }); } } From cacb074835c3646ca9af8a72712f275da69fe720 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 28 Oct 2023 17:17:48 +0200 Subject: [PATCH 21/24] [Sync] Changed CSV-Export warning dialog positive button text --- .../java/de/davis/passwordmanager/ui/backup/BackupFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 3b483767..8b2370cd 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java @@ -101,7 +101,7 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S 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.ok, + .setPositiveButton(R.string.text_continue, (dialog, which) -> { launchAuth(DataTransfer.TYPE_EXPORT, TYPE_CSV); }) From a1a59450d3390e3f2eecea31c26a38f3c61f7d78 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 29 Oct 2023 18:25:32 +0100 Subject: [PATCH 22/24] [KeyGo Sync] Fixed dialog closure on empty password This fix addresses a bug that caused the dialog to close prematurely when no password was entered for exporting or importing a file. Now, users have the opportunity to reenter a password, ensuring a more user-friendly experience. --- .../sync/keygo/KeyGoTransfer.java | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java index ab96f27a..4adc2803 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java @@ -1,6 +1,7 @@ package de.davis.passwordmanager.sync.keygo; import android.content.Context; +import android.content.DialogInterface; import android.net.Uri; import android.text.InputType; import android.widget.EditText; @@ -142,22 +143,29 @@ public void start(int type, Uri uri) { i.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); i.setSecret(true); - new EditDialogBuilder(getContext()) + AlertDialog alertDialog = new EditDialogBuilder(getContext()) .setTitle(R.string.password) - .setPositiveButton(R.string.yes, (dialog, which) -> { - String password = ((EditText)((AlertDialog)dialog).findViewById(R.id.textInputEditText)).getText().toString(); - - if(password.isEmpty()){ - ((TextInputLayout)((AlertDialog)dialog).findViewById(R.id.textInputLayout)) - .setError(getContext().getString(R.string.is_not_filled_in)); - return; - } - - start(type, uri, 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(); + start(type, uri, password); + }); } } From 2643123f982c7b0875e5c84e77ede982b026d510 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 29 Oct 2023 22:47:52 +0100 Subject: [PATCH 23/24] [Backup] Enhanced implementation abstraction This introduces a more flexible approach for defining secure, password-encrypted backups, as well as standard CSV backups. Additionally, the password request process has been moved to a lower level within the SecureDataBackup class, further enhancing the implementation. --- .../DataBackup.java} | 106 ++++++++++-------- .../{sync => backup}/Result.java | 6 +- .../backup/SecureDataBackup.java | 70 ++++++++++++ .../csv/CsvBackup.java} | 18 +-- .../keygo/KeyGoBackup.java} | 63 ++--------- .../ui/backup/BackupFragment.java | 40 +++---- 6 files changed, 174 insertions(+), 129 deletions(-) rename app/src/main/java/de/davis/passwordmanager/{sync/DataTransfer.java => backup/DataBackup.java} (68%) rename app/src/main/java/de/davis/passwordmanager/{sync => backup}/Result.java (85%) create mode 100644 app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.java rename app/src/main/java/de/davis/passwordmanager/{sync/csv/CsvTransfer.java => backup/csv/CsvBackup.java} (88%) rename app/src/main/java/de/davis/passwordmanager/{sync/keygo/KeyGoTransfer.java => backup/keygo/KeyGoBackup.java} (66%) diff --git a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java b/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.java similarity index 68% rename from app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java rename to app/src/main/java/de/davis/passwordmanager/backup/DataBackup.java index 3fbdc8a1..dcee037d 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/DataTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.java @@ -1,4 +1,4 @@ -package de.davis.passwordmanager.sync; +package de.davis.passwordmanager.backup; import static de.davis.passwordmanager.utils.BackgroundUtil.doInBackground; @@ -10,7 +10,7 @@ import android.widget.Toast; import androidx.annotation.IntDef; -import androidx.annotation.WorkerThread; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.os.HandlerCompat; @@ -24,9 +24,8 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.dialog.LoadingDialog; -public abstract class DataTransfer { +public abstract class DataBackup { - private final Context context; public static final int TYPE_EXPORT = 0; public static final int TYPE_IMPORT = 1; @@ -34,11 +33,12 @@ public abstract class DataTransfer { @IntDef({TYPE_EXPORT, TYPE_IMPORT}) public @interface Type{} - private LoadingDialog loadingDialog; + private final Context context; + private LoadingDialog loadingDialog; private final Handler handler = HandlerCompat.createAsync(Looper.getMainLooper()); - public DataTransfer(Context context) { + public DataBackup(Context context) { this.context = context; } @@ -46,6 +46,44 @@ 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(); @@ -55,75 +93,45 @@ protected void error(Exception exception){ new MaterialAlertDialogBuilder(getContext()) .setTitle(R.string.error_title) .setMessage(msg) - .setPositiveButton(R.string.ok, - (dialog, which) -> dialog.dismiss()) + .setPositiveButton(R.string.ok, (dialog, which) -> {}) .show(); }); + exception.printStackTrace(); } - protected void handleResult(Result result){ + 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) -> {}) + .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) -> {}) + .setPositiveButton(R.string.ok, (dialog, which) -> handleSyncHandler(onSyncedHandler, result)) .show(); - else if (result instanceof Result.Success success) + 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); + } }); } - - @WorkerThread - protected abstract Result importElements(InputStream inputStream, String password) throws Exception; - @WorkerThread - protected abstract Result exportElements(OutputStream outputStream, String password) throws Exception; - - public void start(@Type int type, Uri uri){ - start(type, uri, null); + 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)); } - protected void start(@Type int type, Uri uri, String password) { - 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 = exportElements(resolver.openOutputStream(uri), password); - case TYPE_IMPORT -> result = importElements(resolver.openInputStream(uri), password); - } - - if(result == null) - return; - - handleResult(result); - }catch (Exception e){ - if(e instanceof NullPointerException) - return; - - error(e); - }finally { - alertDialog.dismiss(); - } - }); + public interface OnSyncedHandler { + void onSynced(Result result); } } diff --git a/app/src/main/java/de/davis/passwordmanager/sync/Result.java b/app/src/main/java/de/davis/passwordmanager/backup/Result.java similarity index 85% rename from app/src/main/java/de/davis/passwordmanager/sync/Result.java rename to app/src/main/java/de/davis/passwordmanager/backup/Result.java index f5de5e12..199eee8b 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/Result.java +++ b/app/src/main/java/de/davis/passwordmanager/backup/Result.java @@ -1,11 +1,11 @@ -package de.davis.passwordmanager.sync; +package de.davis.passwordmanager.backup; -import static de.davis.passwordmanager.sync.DataTransfer.TYPE_IMPORT; +import static de.davis.passwordmanager.backup.DataBackup.TYPE_IMPORT; public class Result { public static class Success extends Result { - @DataTransfer.Type + @DataBackup.Type private int type; public Success(int type) { diff --git a/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.java b/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.java new file mode 100644 index 00000000..54c6a103 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.java @@ -0,0 +1,70 @@ +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/sync/csv/CsvTransfer.java b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.java similarity index 88% rename from app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java rename to app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.java index 4beb29b6..196efc18 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/csv/CsvTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.java @@ -1,7 +1,9 @@ -package de.davis.passwordmanager.sync.csv; +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; @@ -16,22 +18,23 @@ 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.SecureElementDatabase; import de.davis.passwordmanager.security.element.SecureElement; import de.davis.passwordmanager.security.element.SecureElementManager; import de.davis.passwordmanager.security.element.password.PasswordDetails; -import de.davis.passwordmanager.sync.DataTransfer; -import de.davis.passwordmanager.sync.Result; -public class CsvTransfer extends DataTransfer { +public class CsvBackup extends DataBackup { - public CsvTransfer(Context context) { + public CsvBackup(Context context) { super(context); } + @NonNull @Override - protected Result importElements(InputStream inputStream, String password) throws Exception { + 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))) @@ -74,8 +77,9 @@ protected Result importElements(InputStream inputStream, String password) throws return new Result.Success(TYPE_IMPORT); } + @NonNull @Override - protected Result exportElements(OutputStream outputStream, String password) throws Exception { + protected Result runExport(OutputStream outputStream) throws Exception { CSVWriter csvWriter = (CSVWriter) new CSVWriterBuilder(new OutputStreamWriter(outputStream)) .build(); diff --git a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.java similarity index 66% rename from app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java rename to app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.java index 4adc2803..5c987b04 100644 --- a/app/src/main/java/de/davis/passwordmanager/sync/keygo/KeyGoTransfer.java +++ b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.java @@ -1,15 +1,9 @@ -package de.davis.passwordmanager.sync.keygo; +package de.davis.passwordmanager.backup.keygo; import android.content.Context; -import android.content.DialogInterface; -import android.net.Uri; -import android.text.InputType; -import android.widget.EditText; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.content.res.AppCompatResources; +import androidx.annotation.NonNull; -import com.google.android.material.textfield.TextInputLayout; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; @@ -29,8 +23,9 @@ 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.SecureElementDatabase; -import de.davis.passwordmanager.dialog.EditDialogBuilder; import de.davis.passwordmanager.gson.strategies.ExcludeAnnotationStrategy; import de.davis.passwordmanager.security.Cryptography; import de.davis.passwordmanager.security.element.ElementDetail; @@ -38,11 +33,8 @@ import de.davis.passwordmanager.security.element.SecureElementDetail; import de.davis.passwordmanager.security.element.SecureElementManager; import de.davis.passwordmanager.security.element.password.PasswordDetails; -import de.davis.passwordmanager.sync.DataTransfer; -import de.davis.passwordmanager.sync.Result; -import de.davis.passwordmanager.ui.views.InformationView; -public class KeyGoTransfer extends DataTransfer { +public class KeyGoBackup extends SecureDataBackup { public static class ElementDetailTypeAdapter implements JsonSerializer, JsonDeserializer { @@ -72,7 +64,7 @@ public JsonElement serialize(ElementDetail src, java.lang.reflect.Type typeOfSrc private final Gson gson; - public KeyGoTransfer(Context context) { + public KeyGoBackup(Context context) { super(context); this.gson = new GsonBuilder() .registerTypeAdapter(ElementDetail.class, new ElementDetailTypeAdapter()) @@ -80,13 +72,14 @@ public KeyGoTransfer(Context context) { .create(); } + @NonNull @Override - protected Result importElements(InputStream inputStream, String password) throws Exception{ + 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, password); + file = Cryptography.decryptWithPwd(file, getPassword()); List list; try{ list = gson.fromJson(new String(file), new TypeToken>(){}.getType()); @@ -121,8 +114,9 @@ protected Result importElements(InputStream inputStream, String password) throws return new Result.Success(TYPE_IMPORT); } + @NonNull @Override - protected Result exportElements(OutputStream outputStream, String password) throws Exception { + protected Result runExport(OutputStream outputStream) throws Exception { List elements = SecureElementDatabase.getInstance() .getSecureElementDao() .getAllOnce() @@ -130,42 +124,9 @@ protected Result exportElements(OutputStream outputStream, String password) thro String j = gson.toJson(elements); - outputStream.write(Cryptography.encryptWithPwd(j.getBytes(), password)); + outputStream.write(Cryptography.encryptWithPwd(j.getBytes(), getPassword())); outputStream.close(); return new Result.Success(TYPE_EXPORT); } - - @Override - public void start(int type, Uri uri) { - 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(); - start(type, uri, password); - }); - } } 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 index 8b2370cd..935f0543 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java @@ -12,9 +12,9 @@ import de.davis.passwordmanager.PasswordManagerApplication; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.sync.DataTransfer; -import de.davis.passwordmanager.sync.csv.CsvTransfer; -import de.davis.passwordmanager.sync.keygo.KeyGoTransfer; +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.login.LoginActivity; public class BackupFragment extends PreferenceFragmentCompat { @@ -29,8 +29,8 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S addPreferencesFromResource(R.xml.backup_preferences); ActivityResultLauncher csvImportLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> { - CsvTransfer transfer = new CsvTransfer(requireContext()); - transfer.start(DataTransfer.TYPE_IMPORT, result); + CsvBackup backup = new CsvBackup(requireContext()); + backup.execute(DataBackup.TYPE_IMPORT, result); }); ActivityResultLauncher csvExportLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("text/comma-separated-values"), result -> { @@ -38,24 +38,24 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S return; - CsvTransfer transfer = new CsvTransfer(requireContext()); - transfer.start(DataTransfer.TYPE_EXPORT, result); + 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; - KeyGoTransfer transfer = new KeyGoTransfer(requireContext()); - transfer.start(DataTransfer.TYPE_EXPORT, result); + KeyGoBackup backup = new KeyGoBackup(requireContext()); + backup.execute(DataBackup.TYPE_EXPORT, result); }); ActivityResultLauncher keygoImportLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> { if(result == null) return; - KeyGoTransfer transfer = new KeyGoTransfer(requireContext()); - transfer.start(DataTransfer.TYPE_IMPORT, result); + KeyGoBackup backup = new KeyGoBackup(requireContext()); + backup.execute(DataBackup.TYPE_IMPORT, result); }); auth = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { @@ -71,14 +71,16 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S return; switch (data.getInt("type")){ - case DataTransfer.TYPE_EXPORT -> { + case DataBackup.TYPE_EXPORT -> { if(formatType.equals(TYPE_CSV)){ + ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false); csvExportLauncher.launch("keygo-passwords.csv"); }else if(formatType.equals(TYPE_KEYGO)){ + ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false); keygoExportLauncher.launch("elements.keygo"); } } - case DataTransfer.TYPE_IMPORT -> { + case DataBackup.TYPE_IMPORT -> { if(formatType.equals(TYPE_CSV)){ ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false); csvImportLauncher.launch(new String[]{"text/comma-separated-values"}); @@ -93,7 +95,7 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S }); findPreference(getString(R.string.preference_import_csv)).setOnPreferenceClickListener(preference -> { - launchAuth(DataTransfer.TYPE_IMPORT, TYPE_CSV); + launchAuth(DataBackup.TYPE_IMPORT, TYPE_CSV); return true; }); @@ -103,10 +105,10 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S .setMessage(R.string.csv_export_warning) .setPositiveButton(R.string.text_continue, (dialog, which) -> { - launchAuth(DataTransfer.TYPE_EXPORT, TYPE_CSV); + launchAuth(DataBackup.TYPE_EXPORT, TYPE_CSV); }) .setNegativeButton(R.string.use_keygo, (dialog, which) -> { - launchAuth(DataTransfer.TYPE_EXPORT, TYPE_KEYGO); + launchAuth(DataBackup.TYPE_EXPORT, TYPE_KEYGO); }) .setNeutralButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) .show(); @@ -114,17 +116,17 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S }); findPreference(getString(R.string.preference_export_keygo)).setOnPreferenceClickListener(preference -> { - launchAuth(DataTransfer.TYPE_EXPORT, TYPE_KEYGO); + launchAuth(DataBackup.TYPE_EXPORT, TYPE_KEYGO); return true; }); findPreference(getString(R.string.preference_import_keygo)).setOnPreferenceClickListener(preference -> { - launchAuth(DataTransfer.TYPE_IMPORT, TYPE_KEYGO); + launchAuth(DataBackup.TYPE_IMPORT, TYPE_KEYGO); return true; }); } - public void launchAuth(@DataTransfer.Type int type, String format){ + public void launchAuth(@DataBackup.Type int type, String format){ Bundle bundle = new Bundle(); bundle.putInt("type", type); bundle.putString("format_type", format); From f6cf7ca7d55378760bf803fe94619802593f424e Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 29 Oct 2023 23:28:16 +0100 Subject: [PATCH 24/24] [KeyGo Backup] Fixed import issue This addresses an issue that occurred when attempting to import a backup by clicking on a .keygo file. Previously, the application would occasionally throw a "Permission denied" error. --- app/src/main/AndroidManifest.xml | 23 +++++---- .../PasswordManagerApplication.java | 1 + .../element/SecureElementManager.java | 16 +++--- .../passwordmanager/ui/BaseMainActivity.java | 9 ---- .../ui/dashboard/DashboardFragment.java | 3 +- .../ui/login/ChangePasswordFragment.java | 9 +++- .../ui/login/EnterPasswordFragment.java | 4 +- .../ui/login/LoginActivity.java | 9 +--- .../ui/sync/ImportActivity.java | 49 +++++++++++++++++++ app/src/main/res/layout/activity_import.xml | 26 ++++++++++ app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 12 files changed, 111 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.java create mode 100644 app/src/main/res/layout/activity_import.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aab73fe2..c79454c2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,6 +22,20 @@ android:supportsRtl="false" android:theme="@style/AppTheme" tools:targetApi="34"> + + + + + + + + + + + - - - - - - - - - { + SecureElementManager manager = SecureElementManager.getInstance(); + manager.setTriggerDataChanged(sem -> { boolean hasElements = sem.hasElements(); binding.listPane.progress.setVisibility(View.GONE); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/login/ChangePasswordFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/login/ChangePasswordFragment.java index a7f78fd3..e92c543b 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/login/ChangePasswordFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/login/ChangePasswordFragment.java @@ -1,5 +1,6 @@ package de.davis.passwordmanager.ui.login; +import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; @@ -100,7 +101,13 @@ public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { //if(requireActivity().getIntent().getExtras() == null) ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false); - startActivity(new Intent(requireContext(), MainActivity.class).setData(requireActivity().getIntent().getData())); + + boolean intentAuthOnly = requireActivity().getIntent().getBooleanExtra(getString(R.string.preference_authenticate_only), false); + if(intentAuthOnly){ + requireActivity().setResult(Activity.RESULT_OK); + }else + startActivity(new Intent(requireContext(), MainActivity.class)); + requireActivity().finish(); ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(true); } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java index 972f4866..fa5de3e0 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/login/EnterPasswordFragment.java @@ -9,7 +9,6 @@ import android.content.Intent; import android.os.Bundle; import android.service.autofill.FillRequest; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -70,8 +69,7 @@ public Intent onSuccess() { return i; } - startActivity(new Intent(getContext(), MainActivity.class) - .setData(requireActivity().getIntent().getData())); + startActivity(new Intent(getContext(), MainActivity.class)); return null; } }; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/login/LoginActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/login/LoginActivity.java index 10fd7f0a..f03ff322 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/login/LoginActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/login/LoginActivity.java @@ -2,14 +2,12 @@ import android.content.Context; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.database.SecureElementDatabase; import de.davis.passwordmanager.security.MasterPassword; public class LoginActivity extends AppCompatActivity { @@ -22,12 +20,7 @@ protected void onCreate(Bundle savedInstanceState) { if(savedInstanceState != null) return; - SecureElementDatabase.createAndGet(this); - if(getIntent().getBooleanExtra(getString(R.string.preference_authenticate_only), false)){ - getSupportFragmentManager().beginTransaction().replace(R.id.container, new EnterPasswordFragment()).commit(); - return; - } boolean masterPasswordAvailable = MasterPassword.getOne().blockingGet() != null; if(getIntent().getBooleanExtra(getString(R.string.preference_master_password), false)){ @@ -46,7 +39,7 @@ public static Intent getIntentForAuthentication(@NonNull Context context){ public static Intent getIntentForAuthentication(@NonNull Context context, Intent destActivity){ Intent intent = new Intent(context, LoginActivity.class); - intent.putExtra(context.getString(R.string.preference_authenticate_only), MasterPassword.getOne().blockingGet() != null); + intent.putExtra(context.getString(R.string.preference_authenticate_only), true); if(destActivity != null) intent.putExtra(context.getString(R.string.authentication_destination), destActivity); 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 new file mode 100644 index 00000000..cbd1033a --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.java @@ -0,0 +1,49 @@ +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.login.LoginActivity; + +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)); + }); + }); + + auth.launch(LoginActivity.getIntentForAuthentication(this)); + binding.button.setOnClickListener(v -> auth.launch(LoginActivity.getIntentForAuthentication(this))); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_import.xml b/app/src/main/res/layout/activity_import.xml new file mode 100644 index 00000000..a18e6252 --- /dev/null +++ b/app/src/main/res/layout/activity_import.xml @@ -0,0 +1,26 @@ + + + + + +