Skip to content

Commit 6991d09

Browse files
authored
[Merge] KDBX Support (#27)
2 parents 2b0f882 + 9d1487b commit 6991d09

26 files changed

+711
-469
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ dependencies {
121121
implementation("com.github.devnied.emvnfccard:library:3.0.1")
122122
implementation("net.grey-panther:natural-comparator:1.1")
123123

124+
implementation("com.github.keemobile:kotpass:0.6.1")
125+
implementation("com.squareup.okio:okio:3.4.0") //Needed for Meta for kotpass
126+
124127
implementation("androidx.datastore:datastore:1.0.0")
125128

126129
"githubImplementation"("com.squareup.retrofit2:retrofit:2.9.0")

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@
2828
android:name=".ui.sync.ImportActivity"
2929
android:exported="true"
3030
android:theme="@style/AppTheme.NoActionBar">
31+
3132
<intent-filter>
3233
<action android:name="android.intent.action.VIEW" />
3334

3435
<category android:name="android.intent.category.DEFAULT" />
3536

36-
<data android:mimeType="application/octet-stream" />
37-
<data android:pathPattern=".*\\.keygo" />
37+
<data android:mimeType="application/x-kdbx" />
3838
<data android:scheme="content" />
3939
</intent-filter>
4040
</activity>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package de.davis.passwordmanager.backup
2+
3+
enum class BackupOperation {
4+
IMPORT,
5+
EXPORT
6+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package de.davis.passwordmanager.backup
2+
3+
import java.io.InputStream
4+
import java.io.OutputStream
5+
6+
interface BackupResourceProvider {
7+
8+
suspend fun provideInputStream(): InputStream
9+
suspend fun provideOutputStream(): OutputStream
10+
11+
suspend fun getFileName(): String
12+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package de.davis.passwordmanager.backup
2+
3+
sealed interface BackupResult {
4+
5+
open class Success : BackupResult
6+
data class SuccessWithDuplicates(val duplicates: Int) : Success()
7+
}
Lines changed: 32 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,52 @@
11
package de.davis.passwordmanager.backup
22

3-
import android.content.Context
4-
import android.content.DialogInterface
5-
import android.net.Uri
6-
import android.widget.Toast
7-
import androidx.annotation.IntDef
8-
import com.google.android.material.dialog.MaterialAlertDialogBuilder
9-
import de.davis.passwordmanager.R
10-
import de.davis.passwordmanager.dialog.LoadingDialog
3+
import de.davis.passwordmanager.backup.listener.BackupListener
114
import kotlinx.coroutines.Dispatchers
125
import kotlinx.coroutines.withContext
136
import java.io.InputStream
147
import java.io.OutputStream
15-
import javax.crypto.AEADBadTagException
168

9+
abstract class DataBackup(private val backupListener: BackupListener = BackupListener.Empty) {
1710

18-
const val TYPE_EXPORT = 0
19-
const val TYPE_IMPORT = 1
11+
protected abstract suspend fun ProgressContext.runImport(inputStream: InputStream): BackupResult
12+
protected abstract suspend fun ProgressContext.runExport(outputStream: OutputStream): BackupResult
2013

21-
@IntDef(TYPE_EXPORT, TYPE_IMPORT)
22-
annotation class Type
23-
24-
abstract class DataBackup(val context: Context) {
25-
26-
private lateinit var loadingDialog: LoadingDialog
27-
28-
@Throws(Exception::class)
29-
internal abstract suspend fun runExport(outputStream: OutputStream): Result
30-
31-
@Throws(Exception::class)
32-
internal abstract suspend fun runImport(inputStream: InputStream): Result
33-
34-
open suspend fun execute(@Type type: Int, uri: Uri, onSyncedHandler: OnSyncedHandler? = null) {
35-
val resolver = context.contentResolver
36-
loadingDialog = LoadingDialog(context).apply {
37-
setTitle(if (type == TYPE_EXPORT) R.string.export else R.string.import_str)
38-
setMessage(R.string.wait_text)
39-
}
40-
val alertDialog = withContext(Dispatchers.Main) { loadingDialog.show() }
41-
42-
try {
43-
withContext(Dispatchers.IO) {
44-
val result: Result = when (type) {
45-
TYPE_EXPORT -> resolver.openOutputStream(uri)?.use { runExport(it) }!!
46-
47-
TYPE_IMPORT -> resolver.openInputStream(uri)?.use { runImport(it) }!!
48-
49-
else -> Result.Error("Unexpected error occurred")
50-
}
51-
52-
handleResult(result, onSyncedHandler)
14+
private val progressContext = object : ProgressContext {
15+
override suspend fun initiateProgress(maxCount: Int) {
16+
withContext(Dispatchers.Main) {
17+
backupListener.initiateProgress(maxCount)
5318
}
54-
} catch (e: Exception) {
55-
if (e is NullPointerException) return
56-
error(e)
57-
} finally {
58-
alertDialog.dismiss()
5919
}
60-
}
61-
62-
internal suspend fun error(exception: Exception) {
63-
val msg = if (exception is AEADBadTagException)
64-
context.getString(R.string.password_does_not_match)
65-
else
66-
exception.message
6720

68-
withContext(Dispatchers.Main) {
69-
MaterialAlertDialogBuilder(context).apply {
70-
setTitle(R.string.error_title)
71-
setMessage(msg)
72-
setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> }
73-
}.show()
21+
override suspend fun madeProgress(progress: Int) {
22+
withContext(Dispatchers.Main) {
23+
backupListener.onProgressUpdated(progress)
24+
}
7425
}
75-
76-
exception.printStackTrace()
7726
}
7827

79-
private suspend fun handleResult(result: Result, onSyncedHandler: OnSyncedHandler?) =
80-
withContext(Dispatchers.Main) {
81-
if (result is Result.Success) {
82-
Toast.makeText(
83-
context,
84-
if (result.type == TYPE_EXPORT) R.string.backup_stored else R.string.backup_restored,
85-
Toast.LENGTH_LONG
86-
).show()
87-
handleSyncHandler(onSyncedHandler, result)
88-
return@withContext
89-
}
90-
91-
MaterialAlertDialogBuilder(context).apply {
92-
setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int ->
93-
handleSyncHandler(
94-
onSyncedHandler,
95-
result
96-
)
97-
}
98-
99-
if (result is Result.Error) {
100-
setTitle(R.string.error_title)
101-
setMessage(result.message)
102-
} else if (result is Result.Duplicate) {
103-
setTitle(R.string.warning)
104-
setMessage(
105-
context.resources.getQuantityString(
106-
R.plurals.item_existed,
107-
result.count,
108-
result.count
109-
)
110-
)
28+
open suspend fun execute(
29+
backupOperation: BackupOperation,
30+
backupResourceProvider: BackupResourceProvider
31+
) {
32+
backupListener.run {
33+
runCatching {
34+
withContext(Dispatchers.Main) { onStart(backupOperation) }
35+
36+
backupResourceProvider.run {
37+
progressContext.run {
38+
when (backupOperation) {
39+
BackupOperation.IMPORT -> provideInputStream().use { runImport(it) }
40+
BackupOperation.EXPORT -> provideOutputStream().use { runExport(it) }
41+
}
42+
}
11143
}
112-
}.show()
113-
}
114-
115-
private fun handleSyncHandler(onSyncedHandler: OnSyncedHandler?, result: Result) {
116-
onSyncedHandler?.onSynced(result)
117-
}
11844

119-
internal suspend fun notifyUpdate(current: Int, max: Int) {
120-
withContext(Dispatchers.Main) {
121-
loadingDialog.updateProgress(current, max)
45+
}.onSuccess {
46+
withContext(Dispatchers.Main) { onSuccess(backupOperation, it) }
47+
}.onFailure {
48+
withContext(Dispatchers.Main) { onFailure(backupOperation, it) }
49+
}
12250
}
12351
}
124-
125-
interface OnSyncedHandler {
126-
fun onSynced(result: Result?)
127-
}
12852
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package de.davis.passwordmanager.backup
2+
3+
interface PasswordProvider {
4+
5+
suspend operator fun invoke(
6+
backupOperation: BackupOperation,
7+
backupResourceProvider: BackupResourceProvider,
8+
callback: suspend (password: String) -> Unit
9+
)
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package de.davis.passwordmanager.backup
2+
3+
interface ProgressContext {
4+
suspend fun initiateProgress(maxCount: Int)
5+
suspend fun madeProgress(progress: Int)
6+
}

app/src/main/java/de/davis/passwordmanager/backup/Result.kt

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 30 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,39 @@
11
package de.davis.passwordmanager.backup
22

3-
import android.content.Context
4-
import android.content.DialogInterface
5-
import android.net.Uri
6-
import android.text.InputType
7-
import androidx.appcompat.app.AlertDialog
8-
import androidx.appcompat.content.res.AppCompatResources
9-
import com.google.android.material.textfield.TextInputLayout
10-
import de.davis.passwordmanager.R
11-
import de.davis.passwordmanager.dialog.EditDialogBuilder
12-
import de.davis.passwordmanager.ui.views.InformationView.Information
13-
import kotlinx.coroutines.CoroutineScope
14-
import kotlinx.coroutines.Dispatchers
15-
import kotlinx.coroutines.Job
16-
import kotlinx.coroutines.launch
17-
import kotlinx.coroutines.withContext
3+
import de.davis.passwordmanager.backup.listener.BackupListener
4+
import java.io.InputStream
5+
import java.io.OutputStream
186

19-
abstract class SecureDataBackup(context: Context) : DataBackup(context) {
7+
abstract class SecureDataBackup(
8+
private val passwordProvider: PasswordProvider,
9+
backupListener: BackupListener = BackupListener.Empty
10+
) : DataBackup(backupListener) {
2011

21-
lateinit var password: String
12+
private lateinit var password: String
2213

23-
private suspend fun requestPassword(
24-
@Type type: Int,
25-
uri: Uri,
26-
onSyncedHandler: OnSyncedHandler?
27-
) {
28-
val information = Information().apply {
29-
hint = context.getString(R.string.password)
30-
inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
31-
isSecret = true
32-
}
33-
withContext(Dispatchers.Main) {
34-
EditDialogBuilder(context).apply {
35-
setTitle(R.string.password)
36-
setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> }
37-
setButtonListener(
38-
DialogInterface.BUTTON_POSITIVE,
39-
R.string.yes
40-
) { dialog, _, password ->
41-
/*
42-
Needed for the error message that appears when the password (field) is empty.
43-
otherwise the dialog would close itself
44-
*/
14+
protected abstract suspend fun ProgressContext.runImport(
15+
inputStream: InputStream,
16+
password: String
17+
): BackupResult
4518

46-
val alertDialog = dialog as AlertDialog
47-
if (password.isEmpty()) {
48-
alertDialog.findViewById<TextInputLayout>(R.id.textInputLayout)?.error =
49-
context.getString(R.string.is_not_filled_in)
50-
return@setButtonListener
51-
}
52-
alertDialog.dismiss()
53-
this@SecureDataBackup.password = password
54-
CoroutineScope(Job() + Dispatchers.IO).launch {
55-
super.execute(type, uri, onSyncedHandler)
56-
}
57-
}
58-
withInformation(information)
59-
withStartIcon(
60-
AppCompatResources.getDrawable(
61-
context,
62-
R.drawable.ic_baseline_password_24
63-
)
64-
)
65-
setCancelable(type == TYPE_IMPORT)
66-
}.show()
67-
}
68-
}
19+
protected abstract suspend fun ProgressContext.runExport(
20+
outputStream: OutputStream,
21+
password: String
22+
): BackupResult
6923

70-
override suspend fun execute(@Type type: Int, uri: Uri, onSyncedHandler: OnSyncedHandler?) {
71-
requestPassword(type, uri, onSyncedHandler)
24+
final override suspend fun ProgressContext.runImport(inputStream: InputStream): BackupResult =
25+
runImport(inputStream, password)
26+
27+
final override suspend fun ProgressContext.runExport(outputStream: OutputStream): BackupResult =
28+
runExport(outputStream, password)
29+
30+
override suspend fun execute(
31+
backupOperation: BackupOperation,
32+
backupResourceProvider: BackupResourceProvider
33+
) {
34+
passwordProvider(backupOperation, backupResourceProvider) {
35+
password = it
36+
super.execute(backupOperation, backupResourceProvider)
37+
}
7238
}
7339
}

0 commit comments

Comments
 (0)