Skip to content
This repository was archived by the owner on Mar 6, 2025. It is now read-only.

Commit 2640d28

Browse files
authored
Display a descriptive error when import fails (#122)
1 parent f695254 commit 2640d28

File tree

12 files changed

+114
-72
lines changed

12 files changed

+114
-72
lines changed

app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,16 @@ class AuthenticatorRepositoryImpl @Inject constructor(
250250
format: ImportFileFormat,
251251
fileData: IntentManager.FileData,
252252
): ImportDataResult = fileManager.uriToByteArray(fileData.uri)
253-
.map { importManager.import(importFileFormat = format, byteArray = it) }
253+
.map {
254+
importManager
255+
.import(
256+
importFileFormat = format,
257+
byteArray = it
258+
)
259+
}
254260
.fold(
255261
onSuccess = { it },
256-
onFailure = { ImportDataResult.Error }
262+
onFailure = { ImportDataResult.Error() }
257263
)
258264

259265
private suspend fun encodeVaultDataToCsv(fileUri: Uri): ExportDataResult {

app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/ImportManagerImpl.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.bitwarden.authenticator.data.platform.manager.imports
22

33
import com.bitwarden.authenticator.data.authenticator.datasource.disk.AuthenticatorDiskSource
4+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult
45
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult
56
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
67
import com.bitwarden.authenticator.data.platform.manager.imports.parsers.AegisExportParser
@@ -29,14 +30,18 @@ class ImportManagerImpl(
2930
}
3031

3132
return try {
32-
parser.parse(byteArray)
33-
.mapCatching { authenticatorDiskSource.saveItem(*it.toTypedArray()) }
34-
.fold(
35-
onSuccess = { ImportDataResult.Success },
36-
onFailure = { ImportDataResult.Error }
37-
)
33+
when (val parseResult = parser.parse(byteArray)) {
34+
is ExportParseResult.Error -> {
35+
ImportDataResult.Error(parseResult.message)
36+
}
37+
38+
is ExportParseResult.Success -> {
39+
authenticatorDiskSource.saveItem(*parseResult.items.toTypedArray())
40+
ImportDataResult.Success
41+
}
42+
}
3843
} catch (e: Throwable) {
39-
ImportDataResult.Error
44+
ImportDataResult.Error()
4045
}
4146
}
4247
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.bitwarden.authenticator.data.platform.manager.imports.model
2+
3+
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity
4+
import com.bitwarden.authenticator.ui.platform.base.util.Text
5+
6+
/**
7+
* Represents the result of parsing an export file.
8+
*/
9+
sealed class ExportParseResult {
10+
11+
/**
12+
* Indicates the selected file has been successfully parsed.
13+
*/
14+
data class Success(val items: List<AuthenticatorItemEntity>) : ExportParseResult()
15+
16+
/**
17+
* Indicates there was an error while parsing the selected file.
18+
*
19+
* @property message User friendly message displayed to the user, if provided.
20+
*/
21+
data class Error(val message: Text? = null) : ExportParseResult()
22+
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.bitwarden.authenticator.data.platform.manager.imports.model
22

3+
import com.bitwarden.authenticator.ui.platform.base.util.Text
4+
35
/**
46
* Represents the result of a data import operation.
57
*/
68
sealed class ImportDataResult {
79
data object Success : ImportDataResult()
810

9-
data object Error : ImportDataResult()
11+
data class Error(val message: Text? = null) : ImportDataResult()
1012
}

app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/TwoFasJsonExport.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ data class TwoFasJsonExport(
88
val appVersionCode: Int,
99
val appOrigin: String,
1010
val services: List<Service>,
11+
val servicesEncrypted: String?,
1112
val groups: List<Group>,
1213
) {
1314
@Serializable

app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/AegisExportParser.kt

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.Aut
44
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity
55
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
66
import com.bitwarden.authenticator.data.platform.manager.imports.model.AegisJsonExport
7-
import com.bitwarden.authenticator.data.platform.util.asFailure
8-
import com.bitwarden.authenticator.data.platform.util.asSuccess
7+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult
98
import kotlinx.serialization.ExperimentalSerializationApi
109
import kotlinx.serialization.SerializationException
1110
import kotlinx.serialization.json.Json
@@ -16,29 +15,28 @@ import java.util.UUID
1615

1716
class AegisExportParser : ExportParser {
1817
@OptIn(ExperimentalSerializationApi::class)
19-
override fun parse(byteArray: ByteArray): Result<List<AuthenticatorItemEntity>> {
18+
override fun parse(byteArray: ByteArray): ExportParseResult {
2019
val importJson = Json {
2120
ignoreUnknownKeys = true
2221
isLenient = true
2322
explicitNulls = false
2423
}
2524

2625
return try {
27-
importJson
26+
val exportData = importJson
2827
.decodeFromStream<AegisJsonExport>(ByteArrayInputStream(byteArray))
29-
.asSuccess()
30-
.mapCatching { exportData ->
31-
exportData
32-
.db
33-
.entries
34-
.toAuthenticatorItemEntities()
35-
}
28+
ExportParseResult.Success(
29+
items = exportData
30+
.db
31+
.entries
32+
.toAuthenticatorItemEntities(),
33+
)
3634
} catch (e: SerializationException) {
37-
e.asFailure()
35+
ExportParseResult.Error()
3836
} catch (e: IllegalArgumentException) {
39-
e.asFailure()
37+
ExportParseResult.Error()
4038
} catch (e: IOException) {
41-
e.asFailure()
39+
ExportParseResult.Error()
4240
}
4341
}
4442

app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/BitwardenExportParser.kt

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package com.bitwarden.authenticator.data.platform.manager.imports.parsers
22

33
import android.net.Uri
4+
import com.bitwarden.authenticator.R
45
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm
56
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity
67
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
78
import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager
89
import com.bitwarden.authenticator.data.authenticator.manager.model.ExportJsonData
10+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult
911
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
10-
import com.bitwarden.authenticator.data.platform.util.asFailure
11-
import com.bitwarden.authenticator.data.platform.util.asSuccess
12+
import com.bitwarden.authenticator.ui.platform.base.util.asText
1213
import kotlinx.serialization.ExperimentalSerializationApi
1314
import kotlinx.serialization.SerializationException
1415
import kotlinx.serialization.json.Json
@@ -19,37 +20,36 @@ import java.io.IOException
1920
class BitwardenExportParser(
2021
private val fileFormat: ImportFileFormat,
2122
) : ExportParser {
22-
override fun parse(byteArray: ByteArray): Result<List<AuthenticatorItemEntity>> {
23+
override fun parse(byteArray: ByteArray): ExportParseResult {
2324
return when (fileFormat) {
2425
ImportFileFormat.BITWARDEN_JSON -> importJsonFile(byteArray)
25-
else -> IllegalArgumentException("Unsupported file format.").asFailure()
26+
else -> ExportParseResult.Error(R.string.import_bitwarden_unsupported_format.asText())
2627
}
2728
}
2829

2930
@OptIn(ExperimentalSerializationApi::class)
30-
private fun importJsonFile(byteArray: ByteArray): Result<List<AuthenticatorItemEntity>> {
31+
private fun importJsonFile(byteArray: ByteArray): ExportParseResult {
3132
val importJson = Json {
3233
ignoreUnknownKeys = true
3334
isLenient = true
3435
explicitNulls = false
3536
}
3637

3738
return try {
38-
importJson
39+
val exportData = importJson
3940
.decodeFromStream<ExportJsonData>(ByteArrayInputStream(byteArray))
40-
.asSuccess()
41-
.mapCatching { exportData ->
42-
exportData
43-
.items
44-
.filter { it.login?.totp != null }
45-
.toAuthenticatorItemEntities()
46-
}
41+
ExportParseResult.Success(
42+
items = exportData
43+
.items
44+
.filter { it.login?.totp != null }
45+
.toAuthenticatorItemEntities()
46+
)
4747
} catch (e: SerializationException) {
48-
e.asFailure()
48+
ExportParseResult.Error()
4949
} catch (e: IllegalArgumentException) {
50-
e.asFailure()
50+
ExportParseResult.Error()
5151
} catch (e: IOException) {
52-
e.asFailure()
52+
ExportParseResult.Error()
5353
}
5454
}
5555

app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/ExportParser.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.bitwarden.authenticator.data.platform.manager.imports.parsers
22

33
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity
4+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult
45

56
/**
67
* Responsible for transforming exported authenticator data to a format consumable by this
@@ -12,5 +13,5 @@ interface ExportParser {
1213
* Converts the given [byteArray] content of a file to a collection of
1314
* [AuthenticatorItemEntity].
1415
*/
15-
fun parse(byteArray: ByteArray): Result<List<AuthenticatorItemEntity>>
16+
fun parse(byteArray: ByteArray): ExportParseResult
1617
}

app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/LastPassExportParser.kt

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ package com.bitwarden.authenticator.data.platform.manager.imports.parsers
33
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm
44
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity
55
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
6+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult
67
import com.bitwarden.authenticator.data.platform.manager.imports.model.LastPassJsonExport
7-
import com.bitwarden.authenticator.data.platform.util.asFailure
8-
import com.bitwarden.authenticator.data.platform.util.asSuccess
98
import kotlinx.serialization.ExperimentalSerializationApi
109
import kotlinx.serialization.SerializationException
1110
import kotlinx.serialization.json.Json
@@ -21,28 +20,27 @@ import java.util.UUID
2120
class LastPassExportParser : ExportParser {
2221

2322
@OptIn(ExperimentalSerializationApi::class)
24-
override fun parse(byteArray: ByteArray): Result<List<AuthenticatorItemEntity>> {
23+
override fun parse(byteArray: ByteArray): ExportParseResult {
2524
val importJson = Json {
2625
ignoreUnknownKeys = true
2726
isLenient = true
2827
explicitNulls = false
2928
}
3029

3130
return try {
32-
importJson
33-
.decodeFromStream<LastPassJsonExport>(ByteArrayInputStream(byteArray))
34-
.asSuccess()
35-
.mapCatching { exportData ->
36-
exportData
37-
.accounts
38-
.toAuthenticatorItemEntities()
39-
}
31+
val exportData =
32+
importJson.decodeFromStream<LastPassJsonExport>(ByteArrayInputStream(byteArray))
33+
ExportParseResult.Success(
34+
items = exportData
35+
.accounts
36+
.toAuthenticatorItemEntities(),
37+
)
4038
} catch (e: SerializationException) {
41-
e.asFailure()
39+
ExportParseResult.Error()
4240
} catch (e: IllegalArgumentException) {
43-
e.asFailure()
41+
ExportParseResult.Error()
4442
} catch (e: IOException) {
45-
e.asFailure()
43+
ExportParseResult.Error()
4644
}
4745
}
4846

app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/TwoFasExportParser.kt

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package com.bitwarden.authenticator.data.platform.manager.imports.parsers
22

3+
import com.bitwarden.authenticator.R
34
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm
45
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity
56
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
7+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult
68
import com.bitwarden.authenticator.data.platform.manager.imports.model.TwoFasJsonExport
7-
import com.bitwarden.authenticator.data.platform.util.asFailure
8-
import com.bitwarden.authenticator.data.platform.util.asSuccess
9+
import com.bitwarden.authenticator.ui.platform.base.util.asText
910
import kotlinx.serialization.ExperimentalSerializationApi
1011
import kotlinx.serialization.SerializationException
1112
import kotlinx.serialization.json.Json
@@ -21,33 +22,37 @@ private const val TOKEN_TYPE_HOTP = "HOTP"
2122
* items.
2223
*/
2324
class TwoFasExportParser : ExportParser {
24-
override fun parse(byteArray: ByteArray): Result<List<AuthenticatorItemEntity>> {
25+
override fun parse(byteArray: ByteArray): ExportParseResult {
2526
return import2fasJson(byteArray)
2627
}
2728

2829
@OptIn(ExperimentalSerializationApi::class)
29-
private fun import2fasJson(byteArray: ByteArray): Result<List<AuthenticatorItemEntity>> {
30+
private fun import2fasJson(byteArray: ByteArray): ExportParseResult {
3031
val importJson = Json {
3132
ignoreUnknownKeys = true
3233
isLenient = true
3334
explicitNulls = false
3435
}
3536

3637
return try {
37-
importJson
38+
val exportData = importJson
3839
.decodeFromStream<TwoFasJsonExport>(ByteArrayInputStream(byteArray))
39-
.asSuccess()
40-
.mapCatching { exportData ->
41-
exportData
42-
.services
43-
.toAuthenticatorItemEntities()
44-
}
40+
41+
if (!exportData.servicesEncrypted.isNullOrEmpty()) {
42+
ExportParseResult.Error(
43+
message = R.string.import_2fas_password_protected_not_supported.asText(),
44+
)
45+
} else {
46+
ExportParseResult.Success(
47+
items = exportData.services.toAuthenticatorItemEntities()
48+
)
49+
}
4550
} catch (e: SerializationException) {
46-
e.asFailure()
51+
ExportParseResult.Error()
4752
} catch (e: IllegalArgumentException) {
48-
e.asFailure()
53+
ExportParseResult.Error()
4954
} catch (e: IOException) {
50-
e.asFailure()
55+
ExportParseResult.Error()
5156
}
5257
}
5358

0 commit comments

Comments
 (0)