diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6bb3860a..0beb6419 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -121,6 +121,9 @@ dependencies {
implementation("com.github.devnied.emvnfccard:library:3.0.1")
implementation("net.grey-panther:natural-comparator:1.1")
+ implementation("com.github.keemobile:kotpass:0.6.1")
+ implementation("com.squareup.okio:okio:3.4.0") //Needed for Meta for kotpass
+
implementation("androidx.datastore:datastore:1.0.0")
"githubImplementation"("com.squareup.retrofit2:retrofit:2.9.0")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9511702a..8e6f2df7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,13 +28,13 @@
android:name=".ui.sync.ImportActivity"
android:exported="true"
android:theme="@style/AppTheme.NoActionBar">
+
-
-
+
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/BackupOperation.kt b/app/src/main/java/de/davis/passwordmanager/backup/BackupOperation.kt
new file mode 100644
index 00000000..264578aa
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/BackupOperation.kt
@@ -0,0 +1,6 @@
+package de.davis.passwordmanager.backup
+
+enum class BackupOperation {
+ IMPORT,
+ EXPORT
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/BackupResourceProvider.kt b/app/src/main/java/de/davis/passwordmanager/backup/BackupResourceProvider.kt
new file mode 100644
index 00000000..f22bec49
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/BackupResourceProvider.kt
@@ -0,0 +1,12 @@
+package de.davis.passwordmanager.backup
+
+import java.io.InputStream
+import java.io.OutputStream
+
+interface BackupResourceProvider {
+
+ suspend fun provideInputStream(): InputStream
+ suspend fun provideOutputStream(): OutputStream
+
+ suspend fun getFileName(): String
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/BackupResult.kt b/app/src/main/java/de/davis/passwordmanager/backup/BackupResult.kt
new file mode 100644
index 00000000..6b720201
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/BackupResult.kt
@@ -0,0 +1,7 @@
+package de.davis.passwordmanager.backup
+
+sealed interface BackupResult {
+
+ open class Success : BackupResult
+ data class SuccessWithDuplicates(val duplicates: Int) : Success()
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.kt
index 95239a54..c07b7106 100644
--- a/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.kt
+++ b/app/src/main/java/de/davis/passwordmanager/backup/DataBackup.kt
@@ -1,128 +1,52 @@
package de.davis.passwordmanager.backup
-import android.content.Context
-import android.content.DialogInterface
-import android.net.Uri
-import android.widget.Toast
-import androidx.annotation.IntDef
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import de.davis.passwordmanager.R
-import de.davis.passwordmanager.dialog.LoadingDialog
+import de.davis.passwordmanager.backup.listener.BackupListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.io.OutputStream
-import javax.crypto.AEADBadTagException
+abstract class DataBackup(private val backupListener: BackupListener = BackupListener.Empty) {
-const val TYPE_EXPORT = 0
-const val TYPE_IMPORT = 1
+ protected abstract suspend fun ProgressContext.runImport(inputStream: InputStream): BackupResult
+ protected abstract suspend fun ProgressContext.runExport(outputStream: OutputStream): BackupResult
-@IntDef(TYPE_EXPORT, TYPE_IMPORT)
-annotation class Type
-
-abstract class DataBackup(val context: Context) {
-
- private lateinit var loadingDialog: LoadingDialog
-
- @Throws(Exception::class)
- internal abstract suspend fun runExport(outputStream: OutputStream): Result
-
- @Throws(Exception::class)
- internal abstract suspend fun runImport(inputStream: InputStream): Result
-
- open suspend fun execute(@Type type: Int, uri: Uri, onSyncedHandler: OnSyncedHandler? = null) {
- val resolver = context.contentResolver
- loadingDialog = LoadingDialog(context).apply {
- setTitle(if (type == TYPE_EXPORT) R.string.export else R.string.import_str)
- setMessage(R.string.wait_text)
- }
- val alertDialog = withContext(Dispatchers.Main) { loadingDialog.show() }
-
- try {
- withContext(Dispatchers.IO) {
- val result: Result = when (type) {
- TYPE_EXPORT -> resolver.openOutputStream(uri)?.use { runExport(it) }!!
-
- TYPE_IMPORT -> resolver.openInputStream(uri)?.use { runImport(it) }!!
-
- else -> Result.Error("Unexpected error occurred")
- }
-
- handleResult(result, onSyncedHandler)
+ private val progressContext = object : ProgressContext {
+ override suspend fun initiateProgress(maxCount: Int) {
+ withContext(Dispatchers.Main) {
+ backupListener.initiateProgress(maxCount)
}
- } catch (e: Exception) {
- if (e is NullPointerException) return
- error(e)
- } finally {
- alertDialog.dismiss()
}
- }
-
- internal suspend fun error(exception: Exception) {
- val msg = if (exception is AEADBadTagException)
- context.getString(R.string.password_does_not_match)
- else
- exception.message
- withContext(Dispatchers.Main) {
- MaterialAlertDialogBuilder(context).apply {
- setTitle(R.string.error_title)
- setMessage(msg)
- setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> }
- }.show()
+ override suspend fun madeProgress(progress: Int) {
+ withContext(Dispatchers.Main) {
+ backupListener.onProgressUpdated(progress)
+ }
}
-
- exception.printStackTrace()
}
- private suspend fun handleResult(result: Result, onSyncedHandler: OnSyncedHandler?) =
- withContext(Dispatchers.Main) {
- if (result is Result.Success) {
- Toast.makeText(
- context,
- if (result.type == TYPE_EXPORT) R.string.backup_stored else R.string.backup_restored,
- Toast.LENGTH_LONG
- ).show()
- handleSyncHandler(onSyncedHandler, result)
- return@withContext
- }
-
- MaterialAlertDialogBuilder(context).apply {
- setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int ->
- handleSyncHandler(
- onSyncedHandler,
- result
- )
- }
-
- if (result is Result.Error) {
- setTitle(R.string.error_title)
- setMessage(result.message)
- } else if (result is Result.Duplicate) {
- setTitle(R.string.warning)
- setMessage(
- context.resources.getQuantityString(
- R.plurals.item_existed,
- result.count,
- result.count
- )
- )
+ open suspend fun execute(
+ backupOperation: BackupOperation,
+ backupResourceProvider: BackupResourceProvider
+ ) {
+ backupListener.run {
+ runCatching {
+ withContext(Dispatchers.Main) { onStart(backupOperation) }
+
+ backupResourceProvider.run {
+ progressContext.run {
+ when (backupOperation) {
+ BackupOperation.IMPORT -> provideInputStream().use { runImport(it) }
+ BackupOperation.EXPORT -> provideOutputStream().use { runExport(it) }
+ }
+ }
}
- }.show()
- }
-
- private fun handleSyncHandler(onSyncedHandler: OnSyncedHandler?, result: Result) {
- onSyncedHandler?.onSynced(result)
- }
- internal suspend fun notifyUpdate(current: Int, max: Int) {
- withContext(Dispatchers.Main) {
- loadingDialog.updateProgress(current, max)
+ }.onSuccess {
+ withContext(Dispatchers.Main) { onSuccess(backupOperation, it) }
+ }.onFailure {
+ withContext(Dispatchers.Main) { onFailure(backupOperation, it) }
+ }
}
}
-
- interface OnSyncedHandler {
- fun onSynced(result: Result?)
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/PasswordProvider.kt b/app/src/main/java/de/davis/passwordmanager/backup/PasswordProvider.kt
new file mode 100644
index 00000000..c0c68d5f
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/PasswordProvider.kt
@@ -0,0 +1,10 @@
+package de.davis.passwordmanager.backup
+
+interface PasswordProvider {
+
+ suspend operator fun invoke(
+ backupOperation: BackupOperation,
+ backupResourceProvider: BackupResourceProvider,
+ callback: suspend (password: String) -> Unit
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/ProgressContext.kt b/app/src/main/java/de/davis/passwordmanager/backup/ProgressContext.kt
new file mode 100644
index 00000000..1d6d023a
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/ProgressContext.kt
@@ -0,0 +1,6 @@
+package de.davis.passwordmanager.backup
+
+interface ProgressContext {
+ suspend fun initiateProgress(maxCount: Int)
+ suspend fun madeProgress(progress: Int)
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/Result.kt b/app/src/main/java/de/davis/passwordmanager/backup/Result.kt
deleted file mode 100644
index 94d2d102..00000000
--- a/app/src/main/java/de/davis/passwordmanager/backup/Result.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.davis.passwordmanager.backup
-
-sealed class Result {
- open class Success(@field:Type val type: Int) : Result()
- class Error(val message: String) : Result()
- class Duplicate(val count: Int) : Result()
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt
index 66138c84..354425d9 100644
--- a/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt
+++ b/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt
@@ -1,73 +1,39 @@
package de.davis.passwordmanager.backup
-import android.content.Context
-import android.content.DialogInterface
-import android.net.Uri
-import android.text.InputType
-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.Information
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
+import de.davis.passwordmanager.backup.listener.BackupListener
+import java.io.InputStream
+import java.io.OutputStream
-abstract class SecureDataBackup(context: Context) : DataBackup(context) {
+abstract class SecureDataBackup(
+ private val passwordProvider: PasswordProvider,
+ backupListener: BackupListener = BackupListener.Empty
+) : DataBackup(backupListener) {
- lateinit var password: String
+ private lateinit var password: String
- private suspend fun requestPassword(
- @Type type: Int,
- uri: Uri,
- onSyncedHandler: OnSyncedHandler?
- ) {
- val information = Information().apply {
- hint = context.getString(R.string.password)
- inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
- isSecret = true
- }
- withContext(Dispatchers.Main) {
- EditDialogBuilder(context).apply {
- setTitle(R.string.password)
- setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> }
- setButtonListener(
- DialogInterface.BUTTON_POSITIVE,
- R.string.yes
- ) { dialog, _, password ->
- /*
- Needed for the error message that appears when the password (field) is empty.
- otherwise the dialog would close itself
- */
+ protected abstract suspend fun ProgressContext.runImport(
+ inputStream: InputStream,
+ password: String
+ ): BackupResult
- val alertDialog = dialog as AlertDialog
- if (password.isEmpty()) {
- alertDialog.findViewById(R.id.textInputLayout)?.error =
- context.getString(R.string.is_not_filled_in)
- return@setButtonListener
- }
- alertDialog.dismiss()
- this@SecureDataBackup.password = password
- CoroutineScope(Job() + Dispatchers.IO).launch {
- super.execute(type, uri, onSyncedHandler)
- }
- }
- withInformation(information)
- withStartIcon(
- AppCompatResources.getDrawable(
- context,
- R.drawable.ic_baseline_password_24
- )
- )
- setCancelable(type == TYPE_IMPORT)
- }.show()
- }
- }
+ protected abstract suspend fun ProgressContext.runExport(
+ outputStream: OutputStream,
+ password: String
+ ): BackupResult
- override suspend fun execute(@Type type: Int, uri: Uri, onSyncedHandler: OnSyncedHandler?) {
- requestPassword(type, uri, onSyncedHandler)
+ final override suspend fun ProgressContext.runImport(inputStream: InputStream): BackupResult =
+ runImport(inputStream, password)
+
+ final override suspend fun ProgressContext.runExport(outputStream: OutputStream): BackupResult =
+ runExport(outputStream, password)
+
+ override suspend fun execute(
+ backupOperation: BackupOperation,
+ backupResourceProvider: BackupResourceProvider
+ ) {
+ passwordProvider(backupOperation, backupResourceProvider) {
+ password = it
+ super.execute(backupOperation, backupResourceProvider)
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/impl/AndroidBackupListener.kt b/app/src/main/java/de/davis/passwordmanager/backup/impl/AndroidBackupListener.kt
new file mode 100644
index 00000000..aaa30f2b
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/impl/AndroidBackupListener.kt
@@ -0,0 +1,85 @@
+package de.davis.passwordmanager.backup.impl
+
+import android.content.Context
+import android.content.DialogInterface
+import android.widget.Toast
+import app.keemobile.kotpass.errors.CryptoError
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import de.davis.passwordmanager.R
+import de.davis.passwordmanager.backup.BackupOperation
+import de.davis.passwordmanager.backup.BackupResult
+import de.davis.passwordmanager.backup.listener.BackupListener
+import de.davis.passwordmanager.dialog.LoadingDialog
+
+open class AndroidBackupListener(private val context: Context) : BackupListener {
+
+ private lateinit var loadingDialogBuilder: LoadingDialog
+
+ override fun initiateProgress(maxCount: Int) {
+ loadingDialogBuilder.setMax(maxCount)
+ }
+
+ override fun onProgressUpdated(progress: Int) {
+ loadingDialogBuilder.updateProgress(progress)
+ }
+
+ override fun onStart(backupOperation: BackupOperation) {
+ loadingDialogBuilder = LoadingDialog(context).apply {
+ setTitle(if (backupOperation == BackupOperation.EXPORT) R.string.export else R.string.import_str)
+ setMessage(R.string.wait_text)
+ }.also { it.show() }
+ }
+
+ override fun onSuccess(backupOperation: BackupOperation, backupResult: BackupResult) {
+ loadingDialogBuilder.dismiss()
+ when (backupResult) {
+ is BackupResult.SuccessWithDuplicates -> {
+ MaterialAlertDialogBuilder(context).apply {
+ setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> }
+
+ setTitle(R.string.warning)
+ setMessage(
+ context.resources.getQuantityString(
+ R.plurals.item_existed,
+ backupResult.duplicates,
+ backupResult.duplicates
+ )
+ )
+ }.show()
+ }
+
+ is BackupResult.Success -> {
+ Toast.makeText(context, R.string.backup_restored, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ override fun onFailure(backupOperation: BackupOperation, throwable: Throwable) {
+ loadingDialogBuilder.dismiss()
+
+ val msg = when (throwable) {
+ is CryptoError.InvalidKey -> context.getString(R.string.password_does_not_match)
+ else -> {
+ when (throwable.message?.trim()) {
+ MSG_ROW_NUMBER_ERROR -> {
+ context.getString(R.string.csv_row_number_error)
+ }
+
+ else -> throwable.message
+ }
+ }
+ }
+
+ MaterialAlertDialogBuilder(context).apply {
+ setTitle(R.string.error_title)
+ setMessage(msg)
+ setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> }
+ }.show()
+
+ throwable.printStackTrace()
+ }
+
+ companion object {
+ const val MSG_ROW_NUMBER_ERROR = "error_row_number"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/impl/AndroidPasswordProvider.kt b/app/src/main/java/de/davis/passwordmanager/backup/impl/AndroidPasswordProvider.kt
new file mode 100644
index 00000000..dc407fd7
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/impl/AndroidPasswordProvider.kt
@@ -0,0 +1,85 @@
+package de.davis.passwordmanager.backup.impl
+
+import android.content.Context
+import android.content.DialogInterface
+import android.os.IBinder
+import android.text.InputType
+import android.view.inputmethod.InputMethodManager
+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.backup.BackupOperation
+import de.davis.passwordmanager.backup.BackupResourceProvider
+import de.davis.passwordmanager.backup.PasswordProvider
+import de.davis.passwordmanager.dialog.EditDialogBuilder
+import de.davis.passwordmanager.ui.views.InformationView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class AndroidPasswordProvider(private val context: Context) : PasswordProvider {
+ override suspend fun invoke(
+ backupOperation: BackupOperation,
+ backupResourceProvider: BackupResourceProvider,
+ callback: suspend (password: String) -> Unit
+ ) {
+ withContext(Dispatchers.Main) {
+ val information = InformationView.Information().apply {
+ hint = context.getString(R.string.password)
+ inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
+ isSecret = true
+ }
+
+ EditDialogBuilder(context).apply {
+ setTitle(R.string.password)
+ setMessage(
+ context.getString(
+ R.string.enter_password_for_s,
+ backupResourceProvider.getFileName()
+ )
+ )
+ setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> }
+ setButtonListener(
+ DialogInterface.BUTTON_POSITIVE,
+ R.string.yes
+ ) { dialog, _, password ->
+ /*
+ Needed for the error message that appears when the password (field) is empty.
+ otherwise the dialog would close itself
+ */
+
+ val alertDialog = dialog as AlertDialog
+ if (password.isEmpty()) {
+ alertDialog.findViewById(R.id.textInputLayout)?.error =
+ context.getString(R.string.is_not_filled_in)
+ return@setButtonListener
+ }
+ hideKeyboardFrom(
+ context,
+ alertDialog.window?.decorView?.windowToken
+ )
+ alertDialog.dismiss()
+ CoroutineScope(Job() + Dispatchers.IO).launch {
+ callback(password)
+ }
+ }
+ withInformation(information)
+ withStartIcon(
+ AppCompatResources.getDrawable(
+ context,
+ R.drawable.ic_baseline_password_24
+ )
+ )
+ setCancelable(backupOperation == BackupOperation.IMPORT)
+ }.show()
+ }
+ }
+
+ private fun hideKeyboardFrom(context: Context, windowToken: IBinder?) {
+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.hideSoftInputFromWindow(windowToken, 0)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/impl/CsvBackup.kt
similarity index 68%
rename from app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt
rename to app/src/main/java/de/davis/passwordmanager/backup/impl/CsvBackup.kt
index cb6b0fed..56df7901 100644
--- a/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt
+++ b/app/src/main/java/de/davis/passwordmanager/backup/impl/CsvBackup.kt
@@ -1,14 +1,12 @@
-package de.davis.passwordmanager.backup.csv
+package de.davis.passwordmanager.backup.impl
-import android.content.Context
import com.opencsv.CSVReaderBuilder
import com.opencsv.CSVWriterBuilder
import com.opencsv.validators.RowFunctionValidator
-import de.davis.passwordmanager.R
+import de.davis.passwordmanager.backup.BackupResult
import de.davis.passwordmanager.backup.DataBackup
-import de.davis.passwordmanager.backup.Result
-import de.davis.passwordmanager.backup.TYPE_EXPORT
-import de.davis.passwordmanager.backup.TYPE_IMPORT
+import de.davis.passwordmanager.backup.ProgressContext
+import de.davis.passwordmanager.backup.listener.BackupListener
import de.davis.passwordmanager.database.ElementType
import de.davis.passwordmanager.database.SecureElementManager
import de.davis.passwordmanager.database.dtos.SecureElement
@@ -18,33 +16,30 @@ import java.io.InputStreamReader
import java.io.OutputStream
import java.io.OutputStreamWriter
-class CsvBackup(context: Context) : DataBackup(context) {
- @Throws(Exception::class)
- override suspend fun runImport(inputStream: InputStream): Result {
+class CsvBackup(backupListener: BackupListener = BackupListener.Empty) :
+ DataBackup(backupListener = backupListener) {
+
+ override suspend fun ProgressContext.runImport(inputStream: InputStream): BackupResult {
val csvReader = CSVReaderBuilder(InputStreamReader(inputStream)).apply {
withSkipLines(1)
withRowValidator(
RowFunctionValidator(
{ s: Array -> s.size == 5 },
- context.getString(R.string.csv_row_number_error)
- )
- )
- withRowValidator(
- RowFunctionValidator(
- { s: Array -> s.size == 5 },
- context.getString(R.string.csv_row_number_error)
+ AndroidBackupListener.MSG_ROW_NUMBER_ERROR
)
)
}.build()
- var line: Array
+ var line: Array = emptyArray()
val elements: List =
SecureElementManager.getSecureElements(ElementType.PASSWORD.typeId)
var existed = 0
csvReader.use {
- while (csvReader.readNext().also { line = it } != null) {
- if (line[0].isEmpty() || line[3].isEmpty()) // name and password must not be empty
+ while (csvReader.readNext()?.also { line = it } != null) {
+ // Skip lines where either the name (index 0) or password (index 3) is empty.
+ // Assumes length validation by RowValidator, thus preventing IndexOutOfBoundsException.
+ if (line[0].isEmpty() || line[3].isEmpty())
continue
val title = line[0]
@@ -65,11 +60,10 @@ class CsvBackup(context: Context) : DataBackup(context) {
SecureElementManager.insertElement(SecureElement(title, details))
}
}
- return if (existed != 0) Result.Duplicate(existed) else Result.Success(TYPE_IMPORT)
+ return if (existed != 0) BackupResult.SuccessWithDuplicates(existed) else BackupResult.Success()
}
- @Throws(Exception::class)
- override suspend fun runExport(outputStream: OutputStream): Result {
+ override suspend fun ProgressContext.runExport(outputStream: OutputStream): BackupResult {
val csvWriter = CSVWriterBuilder(OutputStreamWriter(outputStream)).build()
val elements: List =
@@ -88,6 +82,6 @@ class CsvBackup(context: Context) : DataBackup(context) {
})
it.flush()
}
- return Result.Success(TYPE_EXPORT)
+ return BackupResult.Success()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/impl/KdbxBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/impl/KdbxBackup.kt
new file mode 100644
index 00000000..b7ed411c
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/impl/KdbxBackup.kt
@@ -0,0 +1,128 @@
+package de.davis.passwordmanager.backup.impl
+
+import app.keemobile.kotpass.constants.BasicField
+import app.keemobile.kotpass.cryptography.EncryptedValue
+import app.keemobile.kotpass.database.Credentials
+import app.keemobile.kotpass.database.KeePassDatabase
+import app.keemobile.kotpass.database.decode
+import app.keemobile.kotpass.database.encode
+import app.keemobile.kotpass.database.modifiers.modifyParentGroup
+import app.keemobile.kotpass.database.traverse
+import app.keemobile.kotpass.models.Entry
+import app.keemobile.kotpass.models.EntryFields
+import app.keemobile.kotpass.models.EntryValue
+import app.keemobile.kotpass.models.Meta
+import de.davis.passwordmanager.backup.BackupResult
+import de.davis.passwordmanager.backup.PasswordProvider
+import de.davis.passwordmanager.backup.ProgressContext
+import de.davis.passwordmanager.backup.SecureDataBackup
+import de.davis.passwordmanager.backup.listener.BackupListener
+import de.davis.passwordmanager.database.ElementType
+import de.davis.passwordmanager.database.SecureElementManager
+import de.davis.passwordmanager.database.dtos.SecureElement
+import de.davis.passwordmanager.database.entities.Tag
+import de.davis.passwordmanager.database.entities.details.password.PasswordDetails
+import de.davis.passwordmanager.database.entities.onlyCustoms
+import java.io.InputStream
+import java.io.OutputStream
+import java.util.UUID
+
+class KdbxBackup(
+ passwordProvider: PasswordProvider,
+ backupListener: BackupListener = BackupListener.Empty,
+) : SecureDataBackup(passwordProvider, backupListener) {
+ private val credentialFactory: (String) -> Credentials = {
+ Credentials.from(EncryptedValue.fromString(it))
+ }
+
+ override suspend fun ProgressContext.runImport(
+ inputStream: InputStream,
+ password: String
+ ): BackupResult {
+ val database = inputStream.use { KeePassDatabase.decode(it, credentialFactory(password)) }
+
+ val existingElements = SecureElementManager.getSecureElements(ElementType.PASSWORD.typeId)
+ val elementsToAdd = mutableListOf()
+
+ var count = 0
+ database.traverse {
+ when (it) {
+ is Entry -> {
+ it.fields.run {
+ if (title?.content == null || this.password?.content == null)
+ return@run
+
+ val element = SecureElement(
+ title?.content!!,
+ PasswordDetails(
+ this.password?.content!!,
+ url?.content.orEmpty(),
+ userName?.content.orEmpty()
+ ),
+ it.tags.map { tagName -> Tag(tagName) },
+ )
+
+ if (existingElements.any { e -> e.title == element.title && e.detail == element.detail }) {
+ count++
+ } else {
+ elementsToAdd += element
+ }
+ }
+ }
+
+ else -> {}
+ }
+ }
+
+ initiateProgress(elementsToAdd.size)
+ elementsToAdd.forEach {
+ SecureElementManager.insertElement(it)
+ madeProgress(elementsToAdd.indexOf(it) + 1)
+ }
+
+ return if (count == 0) BackupResult.Success() else BackupResult.SuccessWithDuplicates(count)
+ }
+
+ override suspend fun ProgressContext.runExport(
+ outputStream: OutputStream,
+ password: String
+ ): BackupResult {
+ val elements = SecureElementManager.getSecureElements(ElementType.PASSWORD.typeId)
+ val database = KeePassDatabase.Ver4x.create("", META, credentialFactory(password)).run {
+ modifyParentGroup {
+ copy(entries = elements.map {
+ val details = it.detail as PasswordDetails
+ Entry(
+ UUID.randomUUID(),
+ fields = EntryFields.of(
+ BasicField.Title() to EntryValue.Plain(it.title),
+ BasicField.Url() to EntryValue.Plain(details.origin),
+ BasicField.UserName() to EntryValue.Plain(details.username),
+ BasicField.Password() to EntryValue.Encrypted(
+ EncryptedValue.fromString(details.password)
+ ),
+ ),
+ tags = it.tags.onlyCustoms().map { tag -> tag.name }
+ )
+ })
+ }
+ }
+
+ outputStream.use {
+ database.encode(it)
+ }
+
+ return BackupResult.Success()
+ }
+
+ companion object {
+ private val META = Meta(
+ generator = "KeyGo - Digital Vault",
+ name = "Elements",
+ description = "Securely stored elements in KeyGo Digital Vault. This database " +
+ "contains sensitive and encrypted data items such as passwords, personal " +
+ "information, and secure notes, meticulously organized for optimal security " +
+ "and accessibility.",
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/impl/UriBackupResourceProvider.kt b/app/src/main/java/de/davis/passwordmanager/backup/impl/UriBackupResourceProvider.kt
new file mode 100644
index 00000000..7427d37e
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/impl/UriBackupResourceProvider.kt
@@ -0,0 +1,44 @@
+package de.davis.passwordmanager.backup.impl
+
+import android.annotation.SuppressLint
+import android.content.ContentResolver
+import android.database.Cursor
+import android.net.Uri
+import android.provider.OpenableColumns
+import de.davis.passwordmanager.backup.BackupResourceProvider
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.InputStream
+import java.io.OutputStream
+
+class UriBackupResourceProvider(
+ private var uri: Uri,
+ private var contentResolver: ContentResolver
+) : BackupResourceProvider {
+
+ override suspend fun provideInputStream(): InputStream = contentResolver.openInputStream(uri)
+ ?: throw IllegalStateException("ContentResolver returned null")
+
+ override suspend fun provideOutputStream(): OutputStream = contentResolver.openOutputStream(uri)
+ ?: throw IllegalStateException("ContentResolver returned null")
+
+ override suspend fun getFileName(): String = contentResolver.getFileName(uri) ?: "Unknown"
+
+ private suspend fun ContentResolver.getFileName(uri: Uri): String? =
+ withContext(Dispatchers.IO) {
+ var fileName: String? = null
+ if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
+ val cursor: Cursor? = query(uri, null, null, null, null)
+ cursor?.use {
+ if (it.moveToFirst()) {
+ @SuppressLint("Range")
+ fileName = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
+ }
+ }
+ } else if (uri.scheme == ContentResolver.SCHEME_FILE) {
+ fileName = uri.lastPathSegment
+ }
+
+ fileName
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt
deleted file mode 100644
index e0f81b34..00000000
--- a/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-package de.davis.passwordmanager.backup.keygo
-
-import android.content.Context
-import com.google.gson.GsonBuilder
-import com.google.gson.JsonArray
-import com.google.gson.JsonDeserializationContext
-import com.google.gson.JsonDeserializer
-import com.google.gson.JsonElement
-import com.google.gson.JsonParseException
-import com.google.gson.JsonSerializationContext
-import com.google.gson.JsonSerializer
-import com.google.gson.reflect.TypeToken
-import de.davis.passwordmanager.R
-import de.davis.passwordmanager.backup.Result
-import de.davis.passwordmanager.backup.SecureDataBackup
-import de.davis.passwordmanager.backup.TYPE_EXPORT
-import de.davis.passwordmanager.backup.TYPE_IMPORT
-import de.davis.passwordmanager.database.ElementType
-import de.davis.passwordmanager.database.SecureElementManager
-import de.davis.passwordmanager.database.dtos.SecureElement
-import de.davis.passwordmanager.database.entities.details.ElementDetail
-import de.davis.passwordmanager.database.entities.details.password.PasswordDetails
-import de.davis.passwordmanager.gson.strategies.ExcludeAnnotationStrategy
-import de.davis.passwordmanager.security.Cryptography
-import org.apache.commons.io.IOUtils
-import java.io.InputStream
-import java.io.OutputStream
-import java.lang.reflect.Type
-
-class KeyGoBackup(context: Context) : SecureDataBackup(context) {
-
- class ElementDetailTypeAdapter : JsonSerializer,
- JsonDeserializer {
- @Throws(JsonParseException::class)
- override fun deserialize(
- json: JsonElement,
- typeOfT: Type,
- context: JsonDeserializationContext
- ): ElementDetail {
- json.asJsonObject.run {
- val type = this["type"].asInt
- if (type == ElementType.PASSWORD.typeId) {
- val passwordArray = JsonArray().apply {
- for (b in Cryptography.encryptAES(this@run["password"].asString.toByteArray())) {
- add(b)
- }
- }
-
- add("password", passwordArray)
- }
-
- return context.deserialize(
- json,
- ElementType.getTypeByTypeId(type).elementDetailClass
- )
- }
- }
-
- override fun serialize(
- src: ElementDetail,
- typeOfSrc: Type,
- context: JsonSerializationContext
- ): JsonElement {
- val jsonObject = context.serialize(src)
- jsonObject.asJsonObject.run {
- if (src is PasswordDetails) addProperty(
- "password",
- src.password
- )
- addProperty("type", src.elementType.typeId)
- }
-
- return jsonObject
- }
- }
-
- private val gson = GsonBuilder().apply {
- registerTypeAdapter(ElementDetail::class.java, ElementDetailTypeAdapter())
- setExclusionStrategies(ExcludeAnnotationStrategy())
- }.create()
-
- @Throws(Exception::class)
- override suspend fun runImport(inputStream: InputStream): Result {
- var file = IOUtils.toByteArray(inputStream)
- if (file.isEmpty()) return Result.Error(context.getString(R.string.invalid_file_length))
-
- file = Cryptography.decryptWithPwd(file, password)
-
- val list: List = try {
- gson.fromJson(
- String(file),
- object : TypeToken>() {}.type
- )
- } catch (e: Exception) {
- return Result.Error(context.getString(R.string.invalid_file))
- }
-
- val elements: List = SecureElementManager.getSecureElements()
-
- var existed = 0
- val length = list.size
-
- for (i in 0 until length) {
- val element = list[i]
- if (elements.any { it.title == element.title && it.detail == element.detail }) {
- existed++
- notifyUpdate(i + 1, length)
- continue
- }
- SecureElementManager.insertElement(element)
- notifyUpdate(i + 1, length)
- }
- return if (existed != 0) Result.Duplicate(existed) else Result.Success(TYPE_IMPORT)
- }
-
- @Throws(Exception::class)
- override suspend fun runExport(outputStream: OutputStream): Result {
- val elements: List = SecureElementManager.getSecureElements()
-
- val json = gson.toJson(elements)
-
- outputStream.use {
- it.write(Cryptography.encryptWithPwd(json.toByteArray(), password))
- }
-
- return Result.Success(TYPE_EXPORT)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/backup/listener/BackupListener.kt b/app/src/main/java/de/davis/passwordmanager/backup/listener/BackupListener.kt
new file mode 100644
index 00000000..abe3d169
--- /dev/null
+++ b/app/src/main/java/de/davis/passwordmanager/backup/listener/BackupListener.kt
@@ -0,0 +1,20 @@
+package de.davis.passwordmanager.backup.listener
+
+import de.davis.passwordmanager.backup.BackupOperation
+import de.davis.passwordmanager.backup.BackupResult
+
+interface BackupListener {
+ fun onStart(backupOperation: BackupOperation)
+ fun onSuccess(backupOperation: BackupOperation, backupResult: BackupResult)
+ fun onFailure(backupOperation: BackupOperation, throwable: Throwable)
+
+ fun initiateProgress(maxCount: Int) {}
+
+ fun onProgressUpdated(progress: Int) {}
+
+ data object Empty : BackupListener {
+ override fun onStart(backupOperation: BackupOperation) {}
+ override fun onSuccess(backupOperation: BackupOperation, backupResult: BackupResult) {}
+ override fun onFailure(backupOperation: BackupOperation, throwable: Throwable) {}
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/dialog/LoadingDialog.java b/app/src/main/java/de/davis/passwordmanager/dialog/LoadingDialog.java
index ec5f888b..aa00a179 100644
--- a/app/src/main/java/de/davis/passwordmanager/dialog/LoadingDialog.java
+++ b/app/src/main/java/de/davis/passwordmanager/dialog/LoadingDialog.java
@@ -5,22 +5,31 @@
import android.view.View;
import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
import de.davis.passwordmanager.databinding.LoadingLayoutBinding;
public class LoadingDialog extends BaseDialogBuilder {
private LoadingLayoutBinding binding;
+ private AlertDialog alertDialog;
public LoadingDialog(@NonNull Context context) {
super(context);
setCancelable(false);
}
- public void updateProgress(int current, int max){
- double progress = (current * 100d / max);
+ public void dismiss(){
+ alertDialog.dismiss();
+ }
+
+ public void setMax(int max){
+ binding.progress.setMax(max);
+ }
+
+ public void updateProgress(int progress) {
+ binding.progress.setProgressCompat(progress, true);
binding.progress.setIndeterminate(false);
- binding.progress.setProgressCompat((int) progress, true);
}
@Override
@@ -28,4 +37,10 @@ public View onCreateView(LayoutInflater inflater) {
binding = LoadingLayoutBinding.inflate(inflater);
return binding.getRoot();
}
+
+ @NonNull
+ @Override
+ public AlertDialog create() {
+ return alertDialog = super.create();
+ }
}
diff --git a/app/src/main/java/de/davis/passwordmanager/ktx/Parcelable.kt b/app/src/main/java/de/davis/passwordmanager/ktx/Parcelable.kt
index 8ce46f7a..ac66f3ef 100644
--- a/app/src/main/java/de/davis/passwordmanager/ktx/Parcelable.kt
+++ b/app/src/main/java/de/davis/passwordmanager/ktx/Parcelable.kt
@@ -4,9 +4,8 @@ package de.davis.passwordmanager.ktx
import android.os.Build
import android.os.Bundle
-import android.os.Parcelable
-fun Bundle.getParcelableCompat(key: String, clazz: Class): A? {
+fun Bundle.getParcelableCompat(key: String, clazz: Class): A? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
classLoader = clazz.classLoader
getParcelable(key, clazz)
diff --git a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt
index bf7d0546..9c3c8873 100644
--- a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt
+++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt
@@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Bundle
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
@@ -14,104 +15,137 @@ import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.davis.passwordmanager.PasswordManagerApplication
import de.davis.passwordmanager.R
-import de.davis.passwordmanager.backup.TYPE_EXPORT
-import de.davis.passwordmanager.backup.TYPE_IMPORT
-import de.davis.passwordmanager.backup.Type
-import de.davis.passwordmanager.backup.csv.CsvBackup
-import de.davis.passwordmanager.backup.keygo.KeyGoBackup
+import de.davis.passwordmanager.backup.BackupOperation
+import de.davis.passwordmanager.backup.DataBackup
+import de.davis.passwordmanager.backup.impl.AndroidBackupListener
+import de.davis.passwordmanager.backup.impl.AndroidPasswordProvider
+import de.davis.passwordmanager.backup.impl.CsvBackup
+import de.davis.passwordmanager.backup.impl.KdbxBackup
+import de.davis.passwordmanager.backup.impl.UriBackupResourceProvider
+import de.davis.passwordmanager.ktx.getParcelableCompat
import de.davis.passwordmanager.ui.auth.AuthenticationRequest
import de.davis.passwordmanager.ui.auth.createRequestAuthenticationIntent
import kotlinx.coroutines.launch
-private const val TYPE_KEYGO = "keygo"
-private const val TYPE_CSV = "csv"
-
class BackupFragment : PreferenceFragmentCompat() {
+ private lateinit var kdbxBackup: KdbxBackup
+ private lateinit var csvBackup: CsvBackup
+
private lateinit var csvImportLauncher: ActivityResultLauncher>
private lateinit var csvExportLauncher: ActivityResultLauncher
- private lateinit var keyGoImportLauncher: ActivityResultLauncher>
- private lateinit var keyGoExportLauncher: ActivityResultLauncher
+ private lateinit var kdbxImportLauncher: ActivityResultLauncher>
+ private lateinit var kdbxExportLauncher: ActivityResultLauncher
+
val auth: ActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult? ->
if (result == null) return@registerForActivityResult
val data = result.data?.extras ?: return@registerForActivityResult
- val formatType = data.getString("format_type") ?: return@registerForActivityResult
- when (data.getInt("type")) {
- TYPE_EXPORT -> {
- if (formatType == TYPE_CSV) {
+ val formatType = data.getString(EXTRA_BACKUP_FORMAT) ?: return@registerForActivityResult
+ val backupOperation =
+ data.getParcelableCompat(EXTRA_BACKUP_TYPE, BackupOperation::class.java)
+ ?: return@registerForActivityResult
+
+ when (backupOperation) {
+ BackupOperation.EXPORT -> {
+ if (formatType == BACKUP_FORMAT_CSV) {
(requireActivity().application as PasswordManagerApplication).disableReAuthentication()
- csvExportLauncher.launch("keygo-passwords.csv")
- } else if (formatType == TYPE_KEYGO) {
+ csvExportLauncher.launch(DEFAULT_FILE_NAME_CSV)
+ } else if (formatType == BACKUP_FORMAT_KDBX) {
(requireActivity().application as PasswordManagerApplication).disableReAuthentication()
- keyGoExportLauncher.launch("elements.keygo")
+ kdbxExportLauncher.launch(DEFAULT_FILE_NAME_KDBX)
}
}
- TYPE_IMPORT -> {
- if (formatType == TYPE_CSV) {
+ BackupOperation.IMPORT -> {
+ if (formatType == BACKUP_FORMAT_CSV) {
(requireActivity().application as PasswordManagerApplication).disableReAuthentication()
- csvImportLauncher.launch(arrayOf("text/comma-separated-values"))
- } else if (formatType == TYPE_KEYGO) {
+ csvImportLauncher.launch(arrayOf(MIME_TYPE_CSV))
+ } else if (formatType == BACKUP_FORMAT_KDBX) {
(requireActivity().application as PasswordManagerApplication).disableReAuthentication()
- keyGoImportLauncher.launch(arrayOf("application/octet-stream"))
+ kdbxImportLauncher.launch(arrayOf(MIME_TYPE_KDBX))
}
}
}
}
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- addPreferencesFromResource(R.xml.backup_preferences)
-
- CsvBackup(requireContext()).run {
- csvImportLauncher =
- registerForActivityResult, Uri>(ActivityResultContracts.OpenDocument()) { result: Uri? ->
- result?.let {
- lifecycleScope.launch {
- execute(TYPE_IMPORT, result)
- }
- }
+ @Suppress("UNCHECKED_CAST")
+ private fun DataBackup.useForActivityResultRegistration(backupOperation: BackupOperation): ActivityResultLauncher {
+ val contract = when (backupOperation) {
+ BackupOperation.IMPORT -> ActivityResultContracts.OpenDocument()
+ BackupOperation.EXPORT -> {
+ val mimeType = when (this) {
+ is CsvBackup -> MIME_TYPE_CSV
+ is KdbxBackup -> MIME_TYPE_KDBX
+ else -> throw IllegalStateException("Unregistered DataBackup")
}
- csvExportLauncher =
- registerForActivityResult(ActivityResultContracts.CreateDocument("text/comma-separated-values")) { result: Uri? ->
- result?.let {
- lifecycleScope.launch {
- execute(TYPE_EXPORT, result)
- }
- }
- }
+ ActivityResultContracts.CreateDocument(mimeType)
+ }
}
- KeyGoBackup(requireContext()).run {
- keyGoExportLauncher =
- registerForActivityResult(ActivityResultContracts.CreateDocument("application/octet-stream")) { result: Uri? ->
- result?.let {
- lifecycleScope.launch {
- execute(TYPE_EXPORT, result)
- }
- }
- }
- keyGoImportLauncher =
- registerForActivityResult, Uri>(ActivityResultContracts.OpenDocument()) { result: Uri? ->
- result?.let {
- lifecycleScope.launch {
- execute(TYPE_IMPORT, result)
- }
- }
+ return registerBackupLauncher(contract, this) as ActivityResultLauncher
+ }
+
+ private fun registerBackupLauncher(
+ contract: ActivityResultContract,
+ backup: DataBackup
+ ): ActivityResultLauncher {
+ val createStreamProvider = { uri: Uri ->
+ UriBackupResourceProvider(uri, requireContext().contentResolver)
+ }
+
+ return registerForActivityResult(contract) { uri: Uri? ->
+ uri?.let {
+ lifecycleScope.launch {
+ backup.execute(
+ if (contract is ActivityResultContracts.OpenDocument) BackupOperation.IMPORT else BackupOperation.EXPORT,
+ createStreamProvider(uri)
+ )
}
+ }
}
+ }
+ private fun Preference.addLaunchAuthFunctionality(
+ backupOperation: BackupOperation,
+ bT: String
+ ) {
+ onPreferenceClickListener = Preference.OnPreferenceClickListener { _: Preference? ->
+ launchAuth(backupOperation, bT)
+ true
+ }
+ }
- findPreference(getString(R.string.preference_import_csv))?.onPreferenceClickListener =
- Preference.OnPreferenceClickListener { _: Preference? ->
- launchAuth(TYPE_IMPORT, TYPE_CSV)
- true
- }
+ private fun initiateBackupImpl() {
+ AndroidBackupListener(requireContext()).also {
+ kdbxBackup = KdbxBackup(AndroidPasswordProvider(requireContext()), it)
+ csvBackup = CsvBackup(it)
+ }
+ }
+ private fun initiateLaunchers() {
+ initiateBackupImpl()
+
+ csvImportLauncher = csvBackup.useForActivityResultRegistration(BackupOperation.IMPORT)
+ csvExportLauncher = csvBackup.useForActivityResultRegistration(BackupOperation.EXPORT)
+
+ kdbxImportLauncher = kdbxBackup.useForActivityResultRegistration(BackupOperation.IMPORT)
+ kdbxExportLauncher = kdbxBackup.useForActivityResultRegistration(BackupOperation.EXPORT)
+ }
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ addPreferencesFromResource(R.xml.backup_preferences)
+
+ initiateLaunchers()
+
+ findPreference(getString(R.string.preference_import_csv))?.addLaunchAuthFunctionality(
+ BackupOperation.IMPORT,
+ BACKUP_FORMAT_CSV
+ )
findPreference(getString(R.string.preference_export_csv))?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { _: Preference? ->
MaterialAlertDialogBuilder(
@@ -120,43 +154,54 @@ class BackupFragment : PreferenceFragmentCompat() {
).apply {
setTitle(R.string.warning)
setMessage(R.string.csv_export_warning)
- setPositiveButton(R.string.text_continue) { _: DialogInterface?, _: Int ->
- launchAuth(
- TYPE_EXPORT,
- TYPE_CSV
- )
+ setNegativeButton(R.string.text_continue) { _: DialogInterface?, _: Int ->
+ launchAuth(BackupOperation.EXPORT, BACKUP_FORMAT_CSV)
}
- setNegativeButton(R.string.use_keygo) { _: DialogInterface?, _: Int ->
- launchAuth(
- TYPE_EXPORT,
- TYPE_KEYGO
- )
+ setPositiveButton(R.string.use_kdbx) { _: DialogInterface?, _: Int ->
+ launchAuth(BackupOperation.EXPORT, BACKUP_FORMAT_KDBX)
}
setNeutralButton(R.string.cancel) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
}.show()
true
}
- findPreference(getString(R.string.preference_export_keygo))?.onPreferenceClickListener =
- Preference.OnPreferenceClickListener { _: Preference? ->
- launchAuth(TYPE_EXPORT, TYPE_KEYGO)
- true
- }
- findPreference(getString(R.string.preference_import_keygo))?.onPreferenceClickListener =
- Preference.OnPreferenceClickListener { _: Preference? ->
- launchAuth(TYPE_IMPORT, TYPE_KEYGO)
- true
- }
+ findPreference(getString(R.string.preference_export_kdbx))?.addLaunchAuthFunctionality(
+ BackupOperation.EXPORT,
+ BACKUP_FORMAT_KDBX
+ )
+ findPreference(getString(R.string.preference_import_kdbx))?.addLaunchAuthFunctionality(
+ BackupOperation.IMPORT,
+ BACKUP_FORMAT_KDBX
+ )
}
- private fun launchAuth(@Type type: Int, format: String) {
+ private fun launchAuth(backupOperation: BackupOperation, format: String) {
auth.launch(
requireContext().createRequestAuthenticationIntent(
AuthenticationRequest.Builder().apply {
withMessage(R.string.authenticate_to_proceed)
- withAdditionalExtras(bundleOf("type" to type, "format_type" to format))
+ withAdditionalExtras(
+ bundleOf(
+ EXTRA_BACKUP_TYPE to backupOperation,
+ EXTRA_BACKUP_FORMAT to format
+ )
+ )
}.build()
)
)
}
+
+ companion object {
+ private const val MIME_TYPE_CSV = "text/comma-separated-values"
+ private const val MIME_TYPE_KDBX = "application/octet-stream"
+
+ private const val EXTRA_BACKUP_TYPE = "de.davis.passwordmanager.extra.BACKUP_TYPE"
+ private const val EXTRA_BACKUP_FORMAT = "de.davis.passwordmanager.extra.BACKUP_FORMAT"
+
+ private const val BACKUP_FORMAT_CSV = "type_csv"
+ private const val BACKUP_FORMAT_KDBX = "type_kdbx"
+
+ private const val DEFAULT_FILE_NAME_CSV = "passwords-keygo.csv"
+ private const val DEFAULT_FILE_NAME_KDBX = "elements-keygo.kdbx"
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.kt b/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.kt
index d7d7b04e..0a75bc2d 100644
--- a/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.kt
+++ b/app/src/main/java/de/davis/passwordmanager/ui/sync/ImportActivity.kt
@@ -7,44 +7,74 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
-import de.davis.passwordmanager.backup.DataBackup.OnSyncedHandler
-import de.davis.passwordmanager.backup.Result
-import de.davis.passwordmanager.backup.TYPE_IMPORT
-import de.davis.passwordmanager.backup.keygo.KeyGoBackup
+import de.davis.passwordmanager.R
+import de.davis.passwordmanager.backup.BackupOperation
+import de.davis.passwordmanager.backup.BackupResult
+import de.davis.passwordmanager.backup.impl.AndroidBackupListener
+import de.davis.passwordmanager.backup.impl.AndroidPasswordProvider
+import de.davis.passwordmanager.backup.impl.KdbxBackup
+import de.davis.passwordmanager.backup.impl.UriBackupResourceProvider
import de.davis.passwordmanager.databinding.ActivityImportBinding
import de.davis.passwordmanager.ui.MainActivity
import de.davis.passwordmanager.ui.auth.AuthenticationRequest
import de.davis.passwordmanager.ui.auth.createRequestAuthenticationIntent
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ImportActivity : AppCompatActivity() {
+
+ private val kdbxBackup = KdbxBackup(
+ AndroidPasswordProvider(this),
+ object : AndroidBackupListener(this@ImportActivity) {
+
+ override fun onSuccess(
+ backupOperation: BackupOperation,
+ backupResult: BackupResult
+ ) {
+ super.onSuccess(backupOperation, backupResult)
+ startActivity(
+ Intent(
+ this@ImportActivity,
+ MainActivity::class.java
+ )
+ )
+ finish()
+ }
+ })
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityImportBinding.inflate(
layoutInflater
)
setContentView(binding.root)
+ val fileUri = intent.data ?: run {
+ finish()
+ return
+ }
+
+ val uriBackupResourceProvider = UriBackupResourceProvider(fileUri, contentResolver)
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ binding.informationView.setInformationText(
+ getString(
+ R.string.authenticate_to_import,
+ uriBackupResourceProvider.getFileName()
+ )
+ )
+ }
val auth =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode != RESULT_OK) return@registerForActivityResult
if (intent.action == null) return@registerForActivityResult
if (intent.action != Intent.ACTION_VIEW) return@registerForActivityResult
- val fileUri = intent.data ?: return@registerForActivityResult
-
- KeyGoBackup(this).run {
- lifecycleScope.launch {
- execute(TYPE_IMPORT, fileUri, object : OnSyncedHandler {
- override fun onSynced(result: Result?) {
- startActivity(
- Intent(
- this@ImportActivity,
- MainActivity::class.java
- )
- )
- }
- })
- }
+
+ lifecycleScope.launch {
+ kdbxBackup.execute(
+ BackupOperation.IMPORT,
+ UriBackupResourceProvider(fileUri, contentResolver)
+ )
}
}
val authIntent = createRequestAuthenticationIntent(AuthenticationRequest.JUST_AUTHENTICATE)
diff --git a/app/src/main/res/layout/activity_import.xml b/app/src/main/res/layout/activity_import.xml
index a18e6252..cb60eb14 100644
--- a/app/src/main/res/layout/activity_import.xml
+++ b/app/src/main/res/layout/activity_import.xml
@@ -9,6 +9,7 @@
tools:context=".ui.sync.ImportActivity">
Exportiere oder importiere ein Backup
Export
CSV Datei
- KeyGo Datei
- KEYGO Format verwenden
+ KDBX Datei
+ KDBX 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
+ Importiere Elemente aus einer verschlüsselten .kdbx Datei
+ Exportiere Elemente in eine .kdbx 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!
+ 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 KDBX Dateiformat zu wählen!
- %d Element wurde ignoriert, da es bereits existiert
@@ -149,8 +149,9 @@
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
+ Um \"%s\" laden zu können, ist eine Authentifizierung erforderlich
Authentifizieren um fortzufahren
+ Geben Sie das Passwort für \"%s\" ein
Tag
Tags
diff --git a/app/src/main/res/values/preferences.xml b/app/src/main/res/values/preferences.xml
index 36dd3811..db65b96c 100644
--- a/app/src/main/res/values/preferences.xml
+++ b/app/src/main/res/values/preferences.xml
@@ -7,6 +7,6 @@
license
import_csv
export_csv
- import_keygo
- export_keygo
+ import_kdbx
+ export_kdbx
\ 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 e725f8ec..61ef3657 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -172,14 +172,14 @@
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
+ KDBX File
+ Use KDBX format
+ Import elements using a encrypted .kdbx file
+ Export elements using a .kdbx 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!
+ 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 KDBX file format!
CSV file must have 5 rows.
Backup successfully restored
Backup successfully stored
@@ -191,8 +191,9 @@
Invalid file
This might take a little time. Please be patient
- To load a backup, authentication is required
+ To load \"%s\", authentication is required
Authenticate to proceed
+ Enter password for \"%s\"
Tag
Tags
diff --git a/app/src/main/res/xml/backup_preferences.xml b/app/src/main/res/xml/backup_preferences.xml
index 2ae881a0..c2a41123 100644
--- a/app/src/main/res/xml/backup_preferences.xml
+++ b/app/src/main/res/xml/backup_preferences.xml
@@ -1,34 +1,29 @@
-
+
+ android:key="@string/preference_import_kdbx"
+ android:title="@string/kdbx_file"
+ android:summary="@string/kdbx_summary_import" />
-
+ android:summary="@string/csv_summary_import"/>
-
+
-
+ android:key="@string/preference_export_kdbx"
+ android:title="@string/kdbx_file"
+ android:summary="@string/kdbx_summary_export"/>
-
+ android:summary="@string/csv_summary_export"/>
\ No newline at end of file