diff --git a/app/build.gradle b/app/build.gradle
index 5d5cd006..2894c5e6 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.6.0"
annotationProcessor "androidx.room:room-compiler:2.6.0"
implementation "androidx.room:room-rxjava3:2.6.0"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0be39320..c79454c2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,31 +3,51 @@
xmlns:tools="http://schemas.android.com/tools">
-
+
-
-
+
+
+ tools:targetApi="34">
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+ android:theme="@style/AppTheme.NoActionBar"
+ android:windowSoftInputMode="adjustNothing" />
+
+
+
@@ -44,6 +64,7 @@
+
diff --git a/app/src/main/java/de/davis/passwordmanager/PasswordManagerApplication.java b/app/src/main/java/de/davis/passwordmanager/PasswordManagerApplication.java
index 54bf2999..405b7809 100644
--- a/app/src/main/java/de/davis/passwordmanager/PasswordManagerApplication.java
+++ b/app/src/main/java/de/davis/passwordmanager/PasswordManagerApplication.java
@@ -25,6 +25,7 @@ public class PasswordManagerApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
+ SecureElementDatabase.createAndGet(this);
DynamicColors.applyToActivitiesIfAvailable(this);
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.java b/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.java
new file mode 100644
index 00000000..dcee037d
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.java
@@ -0,0 +1,137 @@
+package de.davis.passwordmanager.backup;
+
+import static de.davis.passwordmanager.utils.BackgroundUtil.doInBackground;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.widget.Toast;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.os.HandlerCompat;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.crypto.AEADBadTagException;
+
+import de.davis.passwordmanager.R;
+import de.davis.passwordmanager.dialog.LoadingDialog;
+
+public abstract class DataBackup {
+
+
+ public static final int TYPE_EXPORT = 0;
+ public static final int TYPE_IMPORT = 1;
+
+ @IntDef({TYPE_EXPORT, TYPE_IMPORT})
+ public @interface Type{}
+
+
+ private final Context context;
+ private LoadingDialog loadingDialog;
+ private final Handler handler = HandlerCompat.createAsync(Looper.getMainLooper());
+
+ public DataBackup(Context context) {
+ this.context = context;
+ }
+
+ public Context getContext() {
+ return context;
+ }
+
+ @Nullable
+ protected abstract Result runExport(OutputStream outputStream) throws Exception;
+ @Nullable
+ protected abstract Result runImport(InputStream inputStream) throws Exception;
+
+ public void execute(@Type int type, @Nullable Uri uri){
+ execute(type, uri, null);
+ }
+
+ public void execute(@Type int type, @Nullable Uri uri, OnSyncedHandler onSyncedHandler){
+ ContentResolver resolver = getContext().getContentResolver();
+
+ loadingDialog = new LoadingDialog(getContext())
+ .setTitle(type == TYPE_EXPORT ? R.string.export : R.string.import_str)
+ .setMessage(R.string.wait_text);
+ AlertDialog alertDialog = loadingDialog.show();
+
+ doInBackground(() -> {
+ Result result = null;
+ try{
+ switch (type){
+ case TYPE_EXPORT -> result = runExport(resolver.openOutputStream(uri));
+ case TYPE_IMPORT -> result = runImport(resolver.openInputStream(uri));
+ }
+
+ handleResult(result, onSyncedHandler);
+ }catch (Exception e){
+ e.printStackTrace();
+ if(e instanceof NullPointerException)
+ return;
+
+ error(e);
+ }finally {
+ alertDialog.dismiss();
+ }
+ });
+ }
+
+ protected void error(Exception exception){
+ handler.post(() -> {
+ String msg = exception.getMessage();
+ if(exception instanceof AEADBadTagException)
+ msg = getContext().getString(R.string.password_does_not_match);
+
+ new MaterialAlertDialogBuilder(getContext())
+ .setTitle(R.string.error_title)
+ .setMessage(msg)
+ .setPositiveButton(R.string.ok, (dialog, which) -> {})
+ .show();
+ });
+ exception.printStackTrace();
+ }
+
+ protected void handleResult(Result result, OnSyncedHandler onSyncedHandler){
+ handler.post(() -> {
+ if(result instanceof Result.Error error)
+ new MaterialAlertDialogBuilder(getContext())
+ .setTitle(R.string.error_title)
+ .setMessage(error.getMessage())
+ .setPositiveButton(R.string.ok, (dialog, which) -> handleSyncHandler(onSyncedHandler, result))
+ .show();
+
+ else if (result instanceof Result.Duplicate duplicate)
+ new MaterialAlertDialogBuilder(getContext())
+ .setTitle(R.string.warning)
+ .setMessage(getContext().getResources().getQuantityString(R.plurals.item_existed, duplicate.getCount(), duplicate.getCount()))
+ .setPositiveButton(R.string.ok, (dialog, which) -> handleSyncHandler(onSyncedHandler, result))
+ .show();
+
+ else if (result instanceof Result.Success success) {
+ Toast.makeText(getContext(), success.getType() == TYPE_EXPORT ? R.string.backup_stored : R.string.backup_restored, Toast.LENGTH_LONG).show();
+ handleSyncHandler(onSyncedHandler, result);
+ }
+ });
+ }
+
+ private void handleSyncHandler(OnSyncedHandler onSyncedHandler, Result result){
+ if(onSyncedHandler != null)
+ onSyncedHandler.onSynced(result);
+ }
+
+ protected void notifyUpdate(int current, int max){
+ handler.post(() -> loadingDialog.updateProgress(current, max));
+ }
+
+ public interface OnSyncedHandler {
+ void onSynced(Result result);
+ }
+}
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/Result.java b/app/src/main/java/de/davis/passwordmanager/backup/Result.java
new file mode 100644
index 00000000..199eee8b
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/Result.java
@@ -0,0 +1,46 @@
+package de.davis.passwordmanager.backup;
+
+import static de.davis.passwordmanager.backup.DataBackup.TYPE_IMPORT;
+
+public class Result {
+ public static class Success extends Result {
+
+ @DataBackup.Type
+ private int type;
+
+ public Success(int type) {
+ this.type = type;
+ }
+
+ public int getType() {
+ return type;
+ }
+ }
+
+ public static class Error extends Result {
+
+ private final String message;
+
+ public Error(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+ }
+
+ public static class Duplicate extends Success {
+
+ private final int count;
+
+ public Duplicate(int count) {
+ super(TYPE_IMPORT);
+ this.count = count;
+ }
+
+ public int getCount() {
+ return count;
+ }
+ }
+}
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/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/backup/csv/CsvBackup.java b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.java
new file mode 100644
index 00000000..196efc18
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.java
@@ -0,0 +1,105 @@
+package de.davis.passwordmanager.backup.csv;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.opencsv.CSVReader;
+import com.opencsv.CSVReaderBuilder;
+import com.opencsv.CSVWriter;
+import com.opencsv.CSVWriterBuilder;
+import com.opencsv.validators.RowFunctionValidator;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import de.davis.passwordmanager.R;
+import de.davis.passwordmanager.backup.DataBackup;
+import de.davis.passwordmanager.backup.Result;
+import de.davis.passwordmanager.database.SecureElementDatabase;
+import de.davis.passwordmanager.security.element.SecureElement;
+import de.davis.passwordmanager.security.element.SecureElementManager;
+import de.davis.passwordmanager.security.element.password.PasswordDetails;
+
+public class CsvBackup extends DataBackup {
+
+
+ public CsvBackup(Context context) {
+ super(context);
+ }
+
+ @NonNull
+ @Override
+ protected Result runImport(InputStream inputStream) throws Exception {
+ CSVReader csvReader = new CSVReaderBuilder(new InputStreamReader(inputStream))
+ .withSkipLines(1)
+ .withRowValidator(new RowFunctionValidator(s -> s.length == 5, getContext().getString(R.string.csv_row_number_error)))
+ .withRowValidator(new RowFunctionValidator(s -> s.length == 5, getContext().getString(R.string.csv_row_number_error)))
+ .build();
+
+ String[] line;
+
+ List elements = 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];
+ String origin = line[1];
+ String username = line[2];
+ String pwd = line[3];
+ if(elements.stream().anyMatch(element -> element.getTitle().equals(title)
+ && ((PasswordDetails)element.getDetail()).getPassword().equals(pwd)
+ && ((PasswordDetails)element.getDetail()).getUsername().equals(username)
+ && ((PasswordDetails)element.getDetail()).getOrigin().equals(origin))) {
+ existed++;
+ continue;
+ }
+
+ PasswordDetails details = new PasswordDetails(pwd, origin, username);
+ SecureElementManager.getInstance().createElement(new SecureElement(details, title));
+ }
+
+ csvReader.close();
+
+ if(existed != 0)
+ return new Result.Duplicate(existed);
+
+ return new Result.Success(TYPE_IMPORT);
+ }
+
+ @NonNull
+ @Override
+ protected Result runExport(OutputStream outputStream) throws Exception {
+ CSVWriter csvWriter = (CSVWriter) new CSVWriterBuilder(new OutputStreamWriter(outputStream))
+ .build();
+
+ List elements = 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(TYPE_EXPORT);
+ }
+}
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.java b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.java
new file mode 100644
index 00000000..5c987b04
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.java
@@ -0,0 +1,132 @@
+package de.davis.passwordmanager.backup.keygo;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.reflect.TypeToken;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import de.davis.passwordmanager.R;
+import de.davis.passwordmanager.backup.Result;
+import de.davis.passwordmanager.backup.SecureDataBackup;
+import de.davis.passwordmanager.database.SecureElementDatabase;
+import de.davis.passwordmanager.gson.strategies.ExcludeAnnotationStrategy;
+import de.davis.passwordmanager.security.Cryptography;
+import de.davis.passwordmanager.security.element.ElementDetail;
+import de.davis.passwordmanager.security.element.SecureElement;
+import de.davis.passwordmanager.security.element.SecureElementDetail;
+import de.davis.passwordmanager.security.element.SecureElementManager;
+import de.davis.passwordmanager.security.element.password.PasswordDetails;
+
+public class KeyGoBackup extends SecureDataBackup {
+
+ public static class ElementDetailTypeAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public ElementDetail deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ int type = json.getAsJsonObject().get("type").getAsInt();
+ if(type == SecureElement.TYPE_PASSWORD) {
+ JsonArray passwordArray = new JsonArray();
+ for (byte b : Cryptography.encryptAES(json.getAsJsonObject().get("password").getAsString().getBytes())) {
+ passwordArray.add(b);
+ }
+ json.getAsJsonObject().add("password", passwordArray);
+ }
+ return context.deserialize(json, SecureElementDetail.getFor(type).getElementDetailClass());
+ }
+
+ @Override
+ public JsonElement serialize(ElementDetail src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) {
+ JsonElement jsonObject = context.serialize(src);
+ if(src instanceof PasswordDetails pwdSrc)
+ jsonObject.getAsJsonObject().addProperty("password", pwdSrc.getPassword());
+
+ jsonObject.getAsJsonObject().addProperty("type", src.getType());
+ return jsonObject;
+ }
+ }
+
+ private final Gson gson;
+
+ public KeyGoBackup(Context context) {
+ super(context);
+ this.gson = new GsonBuilder()
+ .registerTypeAdapter(ElementDetail.class, new ElementDetailTypeAdapter())
+ .setExclusionStrategies(new ExcludeAnnotationStrategy())
+ .create();
+ }
+
+ @NonNull
+ @Override
+ protected Result runImport(InputStream inputStream) throws Exception{
+ byte[] file = IOUtils.toByteArray(inputStream);
+ if(file.length == 0)
+ return new Result.Error(getContext().getString(R.string.invalid_file_length));
+
+ file = Cryptography.decryptWithPwd(file, getPassword());
+ List list;
+ try{
+ list = gson.fromJson(new String(file), new TypeToken>(){}.getType());
+ }catch (Exception e){
+ return new Result.Error(getContext().getString(R.string.invalid_file));
+ }
+
+ List elements = SecureElementDatabase.getInstance()
+ .getSecureElementDao()
+ .getAllOnce()
+ .blockingGet();
+
+ int existed = 0;
+ int length = list.size();
+ for (int i = 0; i < length; i++) {
+ SecureElement element = list.get(i);
+ if(elements.stream().anyMatch(e -> e.getTitle().equals(element.getTitle())
+ && e.getDetail().equals(element.getDetail()))) {
+ existed++;
+
+ notifyUpdate(i+1, length);
+ continue;
+ }
+
+ SecureElementManager.getInstance().createElement(element);
+ notifyUpdate(i+1, length);
+ }
+
+ if(existed != 0)
+ return new Result.Duplicate(existed);
+
+ return new Result.Success(TYPE_IMPORT);
+ }
+
+ @NonNull
+ @Override
+ protected Result runExport(OutputStream outputStream) throws Exception {
+ List elements = SecureElementDatabase.getInstance()
+ .getSecureElementDao()
+ .getAllOnce()
+ .blockingGet();
+
+ String j = gson.toJson(elements);
+
+ outputStream.write(Cryptography.encryptWithPwd(j.getBytes(), getPassword()));
+ outputStream.close();
+
+ return new Result.Success(TYPE_EXPORT);
+ }
+}
diff --git a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementDao.java b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementDao.java
index e8d186a6..d9611bfa 100644
--- a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementDao.java
+++ b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementDao.java
@@ -45,6 +45,9 @@ public void update(SecureElement element){
@Query("SELECT * FROM SecureElement ORDER BY ROWID ASC")
public abstract LiveData> 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/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/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/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/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/SecureElementManager.java b/app/src/main/java/de/davis/passwordmanager/security/element/SecureElementManager.java
index 94b7451b..99a23ee6 100644
--- a/app/src/main/java/de/davis/passwordmanager/security/element/SecureElementManager.java
+++ b/app/src/main/java/de/davis/passwordmanager/security/element/SecureElementManager.java
@@ -12,25 +12,23 @@ public class SecureElementManager {
private static SecureElementManager instance;
private final DashboardAdapter adapter;
- private final TriggerDataChanged triggerDataChanged;
+ private TriggerDataChanged triggerDataChanged;
- private SecureElementManager(TriggerDataChanged triggerDataChanged) {
- this.triggerDataChanged = triggerDataChanged;
+ private SecureElementManager() {
adapter = new DashboardAdapter();
}
- public static SecureElementManager createNew(TriggerDataChanged triggerDataChanged){
- instance = new SecureElementManager(triggerDataChanged);
- return instance;
- }
-
public static SecureElementManager getInstance(){
if(instance == null)
- throw new NullPointerException("call createNew first");
+ instance = new SecureElementManager();
return instance;
}
+ public void setTriggerDataChanged(TriggerDataChanged triggerDataChanged) {
+ this.triggerDataChanged = triggerDataChanged;
+ }
+
public DashboardAdapter getAdapter() {
return adapter;
}
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/ui/backup/BackupFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java
new file mode 100644
index 00000000..935f0543
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.java
@@ -0,0 +1,135 @@
+package de.davis.passwordmanager.ui.backup;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import de.davis.passwordmanager.PasswordManagerApplication;
+import de.davis.passwordmanager.R;
+import de.davis.passwordmanager.backup.DataBackup;
+import de.davis.passwordmanager.backup.csv.CsvBackup;
+import de.davis.passwordmanager.backup.keygo.KeyGoBackup;
+import de.davis.passwordmanager.ui.login.LoginActivity;
+
+public class BackupFragment extends PreferenceFragmentCompat {
+
+ ActivityResultLauncher auth;
+
+ private static final String TYPE_KEYGO = "keygo";
+ private static final String TYPE_CSV = "csv";
+
+ @Override
+ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+ addPreferencesFromResource(R.xml.backup_preferences);
+
+ ActivityResultLauncher csvImportLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> {
+ CsvBackup backup = new CsvBackup(requireContext());
+ backup.execute(DataBackup.TYPE_IMPORT, result);
+ });
+
+ ActivityResultLauncher csvExportLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("text/comma-separated-values"), result -> {
+ if(result == null)
+ return;
+
+
+ CsvBackup backup = new CsvBackup(requireContext());
+ backup.execute(DataBackup.TYPE_EXPORT, result);
+ });
+
+ ActivityResultLauncher keygoExportLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("application/octet-stream"), result -> {
+ if(result == null)
+ return;
+
+ KeyGoBackup backup = new KeyGoBackup(requireContext());
+ backup.execute(DataBackup.TYPE_EXPORT, result);
+ });
+
+ ActivityResultLauncher keygoImportLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> {
+ if(result == null)
+ return;
+
+ KeyGoBackup backup = new KeyGoBackup(requireContext());
+ backup.execute(DataBackup.TYPE_IMPORT, result);
+ });
+
+ auth = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
+ if(result == null || result.getData() == null)
+ return;
+
+ Bundle data = result.getData().getExtras();
+ if(data == null)
+ return;
+
+ String formatType = data.getString("format_type");
+ if(formatType == null)
+ return;
+
+ switch (data.getInt("type")){
+ case DataBackup.TYPE_EXPORT -> {
+ if(formatType.equals(TYPE_CSV)){
+ ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false);
+ csvExportLauncher.launch("keygo-passwords.csv");
+ }else if(formatType.equals(TYPE_KEYGO)){
+ ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false);
+ keygoExportLauncher.launch("elements.keygo");
+ }
+ }
+ case DataBackup.TYPE_IMPORT -> {
+ if(formatType.equals(TYPE_CSV)){
+ ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false);
+ csvImportLauncher.launch(new String[]{"text/comma-separated-values"});
+ }else if(formatType.equals(TYPE_KEYGO)){
+ ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false);
+ keygoImportLauncher.launch(new String[]{"application/octet-stream"});
+ }
+ }
+ }
+
+
+ });
+
+ findPreference(getString(R.string.preference_import_csv)).setOnPreferenceClickListener(preference -> {
+ launchAuth(DataBackup.TYPE_IMPORT, TYPE_CSV);
+ return true;
+ });
+
+ findPreference(getString(R.string.preference_export_csv)).setOnPreferenceClickListener(preference -> {
+ new MaterialAlertDialogBuilder(requireContext(), com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
+ .setTitle(R.string.warning)
+ .setMessage(R.string.csv_export_warning)
+ .setPositiveButton(R.string.text_continue,
+ (dialog, which) -> {
+ launchAuth(DataBackup.TYPE_EXPORT, TYPE_CSV);
+ })
+ .setNegativeButton(R.string.use_keygo, (dialog, which) -> {
+ launchAuth(DataBackup.TYPE_EXPORT, TYPE_KEYGO);
+ })
+ .setNeutralButton(R.string.cancel, (dialog, which) -> dialog.dismiss())
+ .show();
+ return true;
+ });
+
+ findPreference(getString(R.string.preference_export_keygo)).setOnPreferenceClickListener(preference -> {
+ launchAuth(DataBackup.TYPE_EXPORT, TYPE_KEYGO);
+ return true;
+ });
+
+ findPreference(getString(R.string.preference_import_keygo)).setOnPreferenceClickListener(preference -> {
+ launchAuth(DataBackup.TYPE_IMPORT, TYPE_KEYGO);
+ return true;
+ });
+ }
+
+ public void launchAuth(@DataBackup.Type int type, String format){
+ Bundle bundle = new Bundle();
+ bundle.putInt("type", type);
+ bundle.putString("format_type", format);
+ auth.launch(LoginActivity.getIntentForAuthentication(requireContext()).putExtra("data", bundle));
+ }
+}
diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java
index 20d618ac..f0967f4d 100644
--- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java
+++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java
@@ -81,7 +81,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
arm.registerCreate();
arm.registerEdit(null);
- SecureElementManager manager = SecureElementManager.createNew(sem -> {
+ 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 2ef0d46e..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;
@@ -18,6 +19,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 +99,16 @@ 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)
+ //if(requireActivity().getIntent().getExtras() == null)
+ ((PasswordManagerApplication)requireActivity().getApplication()).setShouldAuthenticate(false);
+
+ 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);
}
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..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
@@ -59,6 +59,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));
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..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
@@ -8,7 +8,6 @@
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 {
@@ -21,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)){
@@ -45,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/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/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/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/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 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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/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..3dfad6ef 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -116,4 +116,34 @@
Version
Fehler aufgetreten
+
+ Achtung!
+ Backup
+ Exportiere oder importiere ein Backup
+ Export
+ CSV Datei
+ KeyGo Datei
+ KEYGO Format verwenden
+
+ 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.\n\nEs wird empfohlen, das KEYGO Dateiformat zu wählen!
+
+
+ - %d Element wurde ignoriert, da es bereits existiert
+ - %d Elemente wurden ignoriert, da diese bereits existieren
+
+ Ungültige Datei Länge
+ Ungültige Datei
+
+ Das könnte ein wenig Zeit in Anspruch nehmen. Bitte haben Sie Geduld
+ Um ein Backup laden zu können, ist eine Authentifizierung erforderlich
\ 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..9566c7d1 100644
--- a/app/src/main/res/values/preferences.xml
+++ b/app/src/main/res/values/preferences.xml
@@ -7,4 +7,8 @@
feature_autofill
feature_reauthenticate
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 56cb915b..053b75ec 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -162,4 +162,31 @@
Version
Error occurred
+
+ Attention!
+ Backup
+ Export or Import a backup
+ Import
+ 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.\n\nIt is recommended to choose the KEYGO file format!
+ CSV file must have 5 rows.
+ Backup successfully restored
+ Backup successfully stored
+
+ - %d item was skipped as it already existed
+ - %d items were skipped as they already existed
+
+ Invalid file length
+ Invalid file
+
+ This might take a little time. Please be patient
+ To load a backup, authentication is required
\ 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..2ae881a0
--- /dev/null
+++ b/app/src/main/res/xml/backup_preferences.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..54709bb1 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -22,6 +22,12 @@
android:defaultValue="2"
app:updatesContinuously="true"/>
+
+
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