diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt index 0da6e3ad2..9570a2d14 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryImpl.kt @@ -250,10 +250,16 @@ class AuthenticatorRepositoryImpl @Inject constructor( format: ImportFileFormat, fileData: IntentManager.FileData, ): ImportDataResult = fileManager.uriToByteArray(fileData.uri) - .map { importManager.import(importFileFormat = format, byteArray = it) } + .map { + importManager + .import( + importFileFormat = format, + byteArray = it + ) + } .fold( onSuccess = { it }, - onFailure = { ImportDataResult.Error } + onFailure = { ImportDataResult.Error() } ) private suspend fun encodeVaultDataToCsv(fileUri: Uri): ExportDataResult { diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/ImportManagerImpl.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/ImportManagerImpl.kt index 78c3835ed..f96db0ad2 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/ImportManagerImpl.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/ImportManagerImpl.kt @@ -1,6 +1,7 @@ package com.bitwarden.authenticator.data.platform.manager.imports import com.bitwarden.authenticator.data.authenticator.datasource.disk.AuthenticatorDiskSource +import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat import com.bitwarden.authenticator.data.platform.manager.imports.parsers.AegisExportParser @@ -29,14 +30,18 @@ class ImportManagerImpl( } return try { - parser.parse(byteArray) - .mapCatching { authenticatorDiskSource.saveItem(*it.toTypedArray()) } - .fold( - onSuccess = { ImportDataResult.Success }, - onFailure = { ImportDataResult.Error } - ) + when (val parseResult = parser.parse(byteArray)) { + is ExportParseResult.Error -> { + ImportDataResult.Error(parseResult.message) + } + + is ExportParseResult.Success -> { + authenticatorDiskSource.saveItem(*parseResult.items.toTypedArray()) + ImportDataResult.Success + } + } } catch (e: Throwable) { - ImportDataResult.Error + ImportDataResult.Error() } } } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/ExportParseResult.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/ExportParseResult.kt new file mode 100644 index 000000000..eab9f74b7 --- /dev/null +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/ExportParseResult.kt @@ -0,0 +1,22 @@ +package com.bitwarden.authenticator.data.platform.manager.imports.model + +import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity +import com.bitwarden.authenticator.ui.platform.base.util.Text + +/** + * Represents the result of parsing an export file. + */ +sealed class ExportParseResult { + + /** + * Indicates the selected file has been successfully parsed. + */ + data class Success(val items: List) : ExportParseResult() + + /** + * Indicates there was an error while parsing the selected file. + * + * @property message User friendly message displayed to the user, if provided. + */ + data class Error(val message: Text? = null) : ExportParseResult() +} diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/ImportDataResult.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/ImportDataResult.kt index 342fb9baf..9c21236b7 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/ImportDataResult.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/ImportDataResult.kt @@ -1,10 +1,12 @@ package com.bitwarden.authenticator.data.platform.manager.imports.model +import com.bitwarden.authenticator.ui.platform.base.util.Text + /** * Represents the result of a data import operation. */ sealed class ImportDataResult { data object Success : ImportDataResult() - data object Error : ImportDataResult() + data class Error(val message: Text? = null) : ImportDataResult() } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/TwoFasJsonExport.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/TwoFasJsonExport.kt index 6df34a954..c8066993f 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/TwoFasJsonExport.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/model/TwoFasJsonExport.kt @@ -8,6 +8,7 @@ data class TwoFasJsonExport( val appVersionCode: Int, val appOrigin: String, val services: List, + val servicesEncrypted: String?, val groups: List, ) { @Serializable diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/AegisExportParser.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/AegisExportParser.kt index f3c9d1f07..b5fdcaff9 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/AegisExportParser.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/AegisExportParser.kt @@ -4,8 +4,7 @@ import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.Aut import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType import com.bitwarden.authenticator.data.platform.manager.imports.model.AegisJsonExport -import com.bitwarden.authenticator.data.platform.util.asFailure -import com.bitwarden.authenticator.data.platform.util.asSuccess +import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json @@ -16,7 +15,7 @@ import java.util.UUID class AegisExportParser : ExportParser { @OptIn(ExperimentalSerializationApi::class) - override fun parse(byteArray: ByteArray): Result> { + override fun parse(byteArray: ByteArray): ExportParseResult { val importJson = Json { ignoreUnknownKeys = true isLenient = true @@ -24,21 +23,20 @@ class AegisExportParser : ExportParser { } return try { - importJson + val exportData = importJson .decodeFromStream(ByteArrayInputStream(byteArray)) - .asSuccess() - .mapCatching { exportData -> - exportData - .db - .entries - .toAuthenticatorItemEntities() - } + ExportParseResult.Success( + items = exportData + .db + .entries + .toAuthenticatorItemEntities(), + ) } catch (e: SerializationException) { - e.asFailure() + ExportParseResult.Error() } catch (e: IllegalArgumentException) { - e.asFailure() + ExportParseResult.Error() } catch (e: IOException) { - e.asFailure() + ExportParseResult.Error() } } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/BitwardenExportParser.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/BitwardenExportParser.kt index f4ac38e6d..1ca2cd85b 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/BitwardenExportParser.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/BitwardenExportParser.kt @@ -1,14 +1,15 @@ package com.bitwarden.authenticator.data.platform.manager.imports.parsers import android.net.Uri +import com.bitwarden.authenticator.R import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager import com.bitwarden.authenticator.data.authenticator.manager.model.ExportJsonData +import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat -import com.bitwarden.authenticator.data.platform.util.asFailure -import com.bitwarden.authenticator.data.platform.util.asSuccess +import com.bitwarden.authenticator.ui.platform.base.util.asText import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json @@ -19,15 +20,15 @@ import java.io.IOException class BitwardenExportParser( private val fileFormat: ImportFileFormat, ) : ExportParser { - override fun parse(byteArray: ByteArray): Result> { + override fun parse(byteArray: ByteArray): ExportParseResult { return when (fileFormat) { ImportFileFormat.BITWARDEN_JSON -> importJsonFile(byteArray) - else -> IllegalArgumentException("Unsupported file format.").asFailure() + else -> ExportParseResult.Error(R.string.import_bitwarden_unsupported_format.asText()) } } @OptIn(ExperimentalSerializationApi::class) - private fun importJsonFile(byteArray: ByteArray): Result> { + private fun importJsonFile(byteArray: ByteArray): ExportParseResult { val importJson = Json { ignoreUnknownKeys = true isLenient = true @@ -35,21 +36,20 @@ class BitwardenExportParser( } return try { - importJson + val exportData = importJson .decodeFromStream(ByteArrayInputStream(byteArray)) - .asSuccess() - .mapCatching { exportData -> - exportData - .items - .filter { it.login?.totp != null } - .toAuthenticatorItemEntities() - } + ExportParseResult.Success( + items = exportData + .items + .filter { it.login?.totp != null } + .toAuthenticatorItemEntities() + ) } catch (e: SerializationException) { - e.asFailure() + ExportParseResult.Error() } catch (e: IllegalArgumentException) { - e.asFailure() + ExportParseResult.Error() } catch (e: IOException) { - e.asFailure() + ExportParseResult.Error() } } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/ExportParser.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/ExportParser.kt index 9ba7c2270..aee85dd16 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/ExportParser.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/ExportParser.kt @@ -1,6 +1,7 @@ package com.bitwarden.authenticator.data.platform.manager.imports.parsers import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity +import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult /** * Responsible for transforming exported authenticator data to a format consumable by this @@ -12,5 +13,5 @@ interface ExportParser { * Converts the given [byteArray] content of a file to a collection of * [AuthenticatorItemEntity]. */ - fun parse(byteArray: ByteArray): Result> + fun parse(byteArray: ByteArray): ExportParseResult } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/LastPassExportParser.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/LastPassExportParser.kt index ca18357e1..4eca8e2f6 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/LastPassExportParser.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/LastPassExportParser.kt @@ -3,9 +3,8 @@ package com.bitwarden.authenticator.data.platform.manager.imports.parsers import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType +import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult import com.bitwarden.authenticator.data.platform.manager.imports.model.LastPassJsonExport -import com.bitwarden.authenticator.data.platform.util.asFailure -import com.bitwarden.authenticator.data.platform.util.asSuccess import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json @@ -21,7 +20,7 @@ import java.util.UUID class LastPassExportParser : ExportParser { @OptIn(ExperimentalSerializationApi::class) - override fun parse(byteArray: ByteArray): Result> { + override fun parse(byteArray: ByteArray): ExportParseResult { val importJson = Json { ignoreUnknownKeys = true isLenient = true @@ -29,20 +28,19 @@ class LastPassExportParser : ExportParser { } return try { - importJson - .decodeFromStream(ByteArrayInputStream(byteArray)) - .asSuccess() - .mapCatching { exportData -> - exportData - .accounts - .toAuthenticatorItemEntities() - } + val exportData = + importJson.decodeFromStream(ByteArrayInputStream(byteArray)) + ExportParseResult.Success( + items = exportData + .accounts + .toAuthenticatorItemEntities(), + ) } catch (e: SerializationException) { - e.asFailure() + ExportParseResult.Error() } catch (e: IllegalArgumentException) { - e.asFailure() + ExportParseResult.Error() } catch (e: IOException) { - e.asFailure() + ExportParseResult.Error() } } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/TwoFasExportParser.kt b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/TwoFasExportParser.kt index e86b614d0..844566470 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/TwoFasExportParser.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/imports/parsers/TwoFasExportParser.kt @@ -1,11 +1,12 @@ package com.bitwarden.authenticator.data.platform.manager.imports.parsers +import com.bitwarden.authenticator.R import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType +import com.bitwarden.authenticator.data.platform.manager.imports.model.ExportParseResult import com.bitwarden.authenticator.data.platform.manager.imports.model.TwoFasJsonExport -import com.bitwarden.authenticator.data.platform.util.asFailure -import com.bitwarden.authenticator.data.platform.util.asSuccess +import com.bitwarden.authenticator.ui.platform.base.util.asText import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json @@ -21,12 +22,12 @@ private const val TOKEN_TYPE_HOTP = "HOTP" * items. */ class TwoFasExportParser : ExportParser { - override fun parse(byteArray: ByteArray): Result> { + override fun parse(byteArray: ByteArray): ExportParseResult { return import2fasJson(byteArray) } @OptIn(ExperimentalSerializationApi::class) - private fun import2fasJson(byteArray: ByteArray): Result> { + private fun import2fasJson(byteArray: ByteArray): ExportParseResult { val importJson = Json { ignoreUnknownKeys = true isLenient = true @@ -34,20 +35,24 @@ class TwoFasExportParser : ExportParser { } return try { - importJson + val exportData = importJson .decodeFromStream(ByteArrayInputStream(byteArray)) - .asSuccess() - .mapCatching { exportData -> - exportData - .services - .toAuthenticatorItemEntities() - } + + if (!exportData.servicesEncrypted.isNullOrEmpty()) { + ExportParseResult.Error( + message = R.string.import_2fas_password_protected_not_supported.asText(), + ) + } else { + ExportParseResult.Success( + items = exportData.services.toAuthenticatorItemEntities() + ) + } } catch (e: SerializationException) { - e.asFailure() + ExportParseResult.Error() } catch (e: IllegalArgumentException) { - e.asFailure() + ExportParseResult.Error() } catch (e: IOException) { - e.asFailure() + ExportParseResult.Error() } } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/importing/ImportingViewModel.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/importing/ImportingViewModel.kt index 520a76efe..b0160bce4 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/importing/ImportingViewModel.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/feature/settings/importing/ImportingViewModel.kt @@ -74,7 +74,8 @@ class ImportingViewModel @Inject constructor( private fun handleImportLocationReceive(action: ImportAction.ImportLocationReceive) { mutableStateFlow.update { it.copy(dialogState = ImportState.DialogState.Loading()) } viewModelScope.launch { - val result = authenticatorRepository.importVaultData(state.importFileFormat, action.fileUri) + val result = + authenticatorRepository.importVaultData(state.importFileFormat, action.fileUri) sendAction(ImportAction.Internal.SaveImportDataToUriResultReceive(result)) } } @@ -89,12 +90,13 @@ class ImportingViewModel @Inject constructor( private fun handleSaveImportDataToUriResultReceive(result: ImportDataResult) { when (result) { - ImportDataResult.Error -> { + is ImportDataResult.Error -> { mutableStateFlow.update { it.copy( dialogState = ImportState.DialogState.Error( title = R.string.an_error_has_occurred.asText(), - message = R.string.import_vault_failure.asText(), + message = result.message + ?: R.string.import_vault_failure.asText(), ) ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8e6374866..ccb47dcce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,4 +119,6 @@ Data backup Bitwarden Authenticator data is backed up and can be restored with your regularly scheduled device backups. Learn more + Importing from 2FAS password protected files is not supported. Try again with an exported file that is not password protected. + Importing Bitwarden CSV files is not supported. Try again with an exported JSON file.