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

Commit 663265d

Browse files
authored
[BWA-11] Import 2FAS exports (#95)
1 parent f236577 commit 663265d

19 files changed

+405
-54
lines changed

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.Authentic
77
import com.bitwarden.authenticator.data.authenticator.repository.model.CreateItemResult
88
import com.bitwarden.authenticator.data.authenticator.repository.model.DeleteItemResult
99
import com.bitwarden.authenticator.data.authenticator.repository.model.ExportDataResult
10-
import com.bitwarden.authenticator.data.authenticator.repository.model.ImportDataResult
1110
import com.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult
1211
import com.bitwarden.authenticator.data.authenticator.repository.model.UpdateItemRequest
1312
import com.bitwarden.authenticator.data.authenticator.repository.model.UpdateItemResult
13+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult
14+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
1415
import com.bitwarden.authenticator.data.platform.repository.model.DataState
1516
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportFormat
16-
import com.bitwarden.authenticator.ui.platform.feature.settings.importing.model.ImportFormat
1717
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
1818
import kotlinx.coroutines.flow.Flow
1919
import kotlinx.coroutines.flow.StateFlow
@@ -94,7 +94,7 @@ interface AuthenticatorRepository {
9494
* Attempt to read the user's data from a file
9595
*/
9696
suspend fun importVaultData(
97-
format: ImportFormat,
97+
format: ImportFileFormat,
9898
fileData: IntentManager.FileData,
9999
): ImportDataResult
100100
}

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

+11-8
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.Authentic
1313
import com.bitwarden.authenticator.data.authenticator.repository.model.CreateItemResult
1414
import com.bitwarden.authenticator.data.authenticator.repository.model.DeleteItemResult
1515
import com.bitwarden.authenticator.data.authenticator.repository.model.ExportDataResult
16-
import com.bitwarden.authenticator.data.authenticator.repository.model.ImportDataResult
1716
import com.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult
1817
import com.bitwarden.authenticator.data.authenticator.repository.model.UpdateItemRequest
1918
import com.bitwarden.authenticator.data.authenticator.repository.model.UpdateItemResult
2019
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
20+
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
21+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult
22+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
2123
import com.bitwarden.authenticator.data.platform.repository.model.DataState
2224
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
2325
import com.bitwarden.authenticator.data.platform.repository.util.combineDataStates
2426
import com.bitwarden.authenticator.data.platform.repository.util.map
2527
import com.bitwarden.authenticator.data.platform.util.asSuccess
2628
import com.bitwarden.authenticator.data.platform.util.flatMap
2729
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportFormat
28-
import com.bitwarden.authenticator.ui.platform.feature.settings.importing.model.ImportFormat
2930
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
3031
import kotlinx.coroutines.CoroutineScope
3132
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -60,6 +61,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
6061
private val authenticatorDiskSource: AuthenticatorDiskSource,
6162
private val totpCodeManager: TotpCodeManager,
6263
private val fileManager: FileManager,
64+
private val importManager: ImportManager,
6365
dispatcherManager: DispatcherManager,
6466
) : AuthenticatorRepository {
6567

@@ -243,13 +245,14 @@ class AuthenticatorRepositoryImpl @Inject constructor(
243245
}
244246

245247
override suspend fun importVaultData(
246-
format: ImportFormat,
248+
format: ImportFileFormat,
247249
fileData: IntentManager.FileData,
248-
): ImportDataResult = when (format) {
249-
ImportFormat.JSON -> {
250-
decodeVaultDataFromJson(fileData)
251-
}
252-
}
250+
): ImportDataResult = fileManager.uriToByteArray(fileData.uri)
251+
.map { importManager.import(importFileFormat = format, byteArray = it) }
252+
.fold(
253+
onSuccess = { ImportDataResult.Success },
254+
onFailure = { ImportDataResult.Error }
255+
)
253256

254257
private suspend fun encodeVaultDataToCsv(fileUri: Uri): ExportDataResult {
255258
val headerLine =

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

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager
66
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
77
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepositoryImpl
88
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
9+
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
910
import dagger.Module
1011
import dagger.Provides
1112
import dagger.hilt.InstallIn
@@ -26,10 +27,12 @@ object AuthenticatorRepositoryModule {
2627
dispatcherManager: DispatcherManager,
2728
totpCodeManager: TotpCodeManager,
2829
fileManager: FileManager,
30+
importManager: ImportManager,
2931
): AuthenticatorRepository = AuthenticatorRepositoryImpl(
3032
authenticatorDiskSource,
3133
totpCodeManager,
3234
fileManager,
35+
importManager,
3336
dispatcherManager,
3437
)
3538

app/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/di/PlatformManagerModule.kt

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.bitwarden.authenticator.data.platform.manager.di
22

33
import android.content.Context
4+
import com.bitwarden.authenticator.data.authenticator.datasource.disk.AuthenticatorDiskSource
45
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSource
56
import com.bitwarden.authenticator.data.platform.manager.BiometricsEncryptionManager
67
import com.bitwarden.authenticator.data.platform.manager.BiometricsEncryptionManagerImpl
@@ -12,6 +13,8 @@ import com.bitwarden.authenticator.data.platform.manager.SdkClientManager
1213
import com.bitwarden.authenticator.data.platform.manager.SdkClientManagerImpl
1314
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
1415
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
16+
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
17+
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManagerImpl
1518
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
1619
import dagger.Module
1720
import dagger.Provides
@@ -53,8 +56,15 @@ object PlatformManagerModule {
5356
): BiometricsEncryptionManager = BiometricsEncryptionManagerImpl(settingsDiskSource)
5457

5558
@Provides
59+
@Singleton
5660
fun provideCrashLogsManager(settingsRepository: SettingsRepository): CrashLogsManager =
5761
CrashLogsManagerImpl(
5862
settingsRepository = settingsRepository,
5963
)
64+
65+
@Provides
66+
@Singleton
67+
fun provideImportManager(
68+
authenticatorDiskSource: AuthenticatorDiskSource,
69+
): ImportManager = ImportManagerImpl(authenticatorDiskSource)
6070
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.bitwarden.authenticator.data.platform.manager.imports
2+
3+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult
4+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
5+
6+
/**
7+
* Responsible for managing import of files from various authenticator exports.
8+
*/
9+
interface ImportManager {
10+
11+
/**
12+
* Imports the selected file.
13+
*/
14+
suspend fun import(
15+
importFileFormat: ImportFileFormat,
16+
byteArray: ByteArray,
17+
): ImportDataResult
18+
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.bitwarden.authenticator.data.platform.manager.imports
2+
3+
import com.bitwarden.authenticator.data.authenticator.datasource.disk.AuthenticatorDiskSource
4+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult
5+
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
6+
import com.bitwarden.authenticator.data.platform.manager.imports.parsers.BitwardenExportParser
7+
import com.bitwarden.authenticator.data.platform.manager.imports.parsers.ExportParser
8+
import com.bitwarden.authenticator.data.platform.manager.imports.parsers.TwoFasExportParser
9+
10+
/**
11+
* Default implementation of [ImportManager] for managing importing files exported by various
12+
* authenticator applications.
13+
*/
14+
class ImportManagerImpl(
15+
private val authenticatorDiskSource: AuthenticatorDiskSource,
16+
) : ImportManager {
17+
override suspend fun import(
18+
importFileFormat: ImportFileFormat,
19+
byteArray: ByteArray,
20+
): ImportDataResult {
21+
22+
val parser: ExportParser = when (importFileFormat) {
23+
ImportFileFormat.BITWARDEN_JSON -> BitwardenExportParser(importFileFormat)
24+
ImportFileFormat.TWO_FAS_JSON -> TwoFasExportParser()
25+
}
26+
27+
return parser.parse(byteArray)
28+
.map { authenticatorDiskSource.saveItem(*it.toTypedArray()) }
29+
.fold(
30+
onSuccess = { ImportDataResult.Success },
31+
onFailure = { ImportDataResult.Error }
32+
)
33+
}
34+
}
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.bitwarden.authenticator.data.authenticator.repository.model
1+
package com.bitwarden.authenticator.data.platform.manager.imports.model
22

33
/**
44
* Represents the result of a data import operation.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.bitwarden.authenticator.data.platform.manager.imports.model
2+
3+
/**
4+
* Represents the file formats a user can select to import their vault.
5+
*/
6+
enum class ImportFileFormat(
7+
val mimeType: String,
8+
) {
9+
BITWARDEN_JSON("application/json"),
10+
TWO_FAS_JSON("*/*"),
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.bitwarden.authenticator.data.platform.manager.imports.model
2+
3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
6+
data class TwoFasJsonExport(
7+
val schemaVersion: Int,
8+
val appVersionCode: Int,
9+
val appOrigin: String,
10+
val services: List<Service>,
11+
val groups: List<Group>,
12+
) {
13+
@Serializable
14+
data class Service(
15+
val otp: Otp,
16+
val order: Order,
17+
val updatedAt: Long,
18+
val name: String,
19+
val icon: Icon?,
20+
val secret: String,
21+
val badge: Badge,
22+
val serviceTypeId: String?,
23+
) {
24+
@Serializable
25+
data class Otp(
26+
val counter: Int,
27+
val period: Int,
28+
val digits: Int,
29+
val account: String,
30+
val source: String?,
31+
val tokenType: String?,
32+
val algorithm: String?,
33+
val link: String?,
34+
val issuer: String?,
35+
)
36+
37+
@Serializable
38+
data class Order(
39+
val position: Int,
40+
)
41+
42+
@Serializable
43+
data class Icon(
44+
val iconCollection: IconCollection,
45+
val label: Label,
46+
val selected: String,
47+
) {
48+
@Serializable
49+
data class IconCollection(
50+
val id: String,
51+
)
52+
53+
@Serializable
54+
data class Label(
55+
val backgroundColor: String,
56+
val text: String,
57+
)
58+
}
59+
60+
@Serializable
61+
data class Badge(
62+
val color: String,
63+
)
64+
}
65+
66+
@Serializable
67+
data class Group(
68+
val id: String,
69+
val name: String,
70+
val isExpanded: Boolean,
71+
)
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.bitwarden.authenticator.data.platform.manager.imports.parsers
2+
3+
import android.net.Uri
4+
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm
5+
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity
6+
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
7+
import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager
8+
import com.bitwarden.authenticator.data.authenticator.manager.model.ExportJsonData
9+
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 kotlinx.serialization.ExperimentalSerializationApi
13+
import kotlinx.serialization.json.Json
14+
import kotlinx.serialization.json.decodeFromStream
15+
import java.io.ByteArrayInputStream
16+
17+
class BitwardenExportParser(
18+
private val fileFormat: ImportFileFormat,
19+
) : ExportParser {
20+
override fun parse(byteArray: ByteArray): Result<List<AuthenticatorItemEntity>> {
21+
return when (fileFormat) {
22+
ImportFileFormat.BITWARDEN_JSON -> importJsonFile(byteArray)
23+
else -> IllegalArgumentException("Unsupported file format.").asFailure()
24+
}
25+
}
26+
27+
@OptIn(ExperimentalSerializationApi::class)
28+
private fun importJsonFile(byteArray: ByteArray): Result<List<AuthenticatorItemEntity>> {
29+
val importJson = Json {
30+
ignoreUnknownKeys = true
31+
isLenient = true
32+
explicitNulls = false
33+
}
34+
35+
return importJson
36+
.decodeFromStream<ExportJsonData>(ByteArrayInputStream(byteArray))
37+
.asSuccess()
38+
.map { exportData ->
39+
exportData
40+
.items
41+
.toAuthenticatorItemEntities()
42+
}
43+
}
44+
45+
private fun List<ExportJsonData.ExportItem>.toAuthenticatorItemEntities() =
46+
map { it.toAuthenticatorItemEntity() }
47+
48+
private fun ExportJsonData.ExportItem.toAuthenticatorItemEntity(): AuthenticatorItemEntity {
49+
val otpString = login.totp
50+
val otpUri = Uri.parse(otpString)
51+
val type = if (otpString.startsWith(TotpCodeManager.TOTP_CODE_PREFIX)) {
52+
AuthenticatorItemType.TOTP
53+
} else if (otpString.startsWith(TotpCodeManager.STEAM_CODE_PREFIX)) {
54+
AuthenticatorItemType.STEAM
55+
} else {
56+
throw IllegalArgumentException("Unsupported OTP type.")
57+
}
58+
59+
val key = when (type) {
60+
AuthenticatorItemType.TOTP -> {
61+
requireNotNull(otpUri.getQueryParameter(TotpCodeManager.SECRET_PARAM))
62+
}
63+
64+
AuthenticatorItemType.STEAM -> {
65+
requireNotNull(otpUri.authority)
66+
}
67+
}
68+
69+
val algorithm = otpUri.getQueryParameter(TotpCodeManager.ALGORITHM_PARAM)
70+
?: TotpCodeManager.ALGORITHM_DEFAULT.name
71+
72+
val period = otpUri.getQueryParameter(TotpCodeManager.PERIOD_PARAM)
73+
?.toIntOrNull()
74+
?: TotpCodeManager.PERIOD_SECONDS_DEFAULT
75+
76+
val digits = when (type) {
77+
AuthenticatorItemType.TOTP -> {
78+
otpUri.getQueryParameter(TotpCodeManager.DIGITS_PARAM)
79+
?.toIntOrNull()
80+
?: TotpCodeManager.TOTP_DIGITS_DEFAULT
81+
}
82+
83+
AuthenticatorItemType.STEAM -> {
84+
TotpCodeManager.STEAM_DIGITS_DEFAULT
85+
}
86+
}
87+
val issuer = otpUri.getQueryParameter(TotpCodeManager.ISSUER_PARAM)
88+
?: name
89+
90+
val label = when (type) {
91+
AuthenticatorItemType.TOTP -> {
92+
otpUri.pathSegments
93+
.firstOrNull()
94+
.orEmpty()
95+
.removePrefix("$issuer:")
96+
}
97+
98+
AuthenticatorItemType.STEAM -> null
99+
}
100+
101+
return AuthenticatorItemEntity(
102+
id = id,
103+
key = key,
104+
type = type,
105+
algorithm = algorithm.let { AuthenticatorItemAlgorithm.valueOf(it) },
106+
period = period,
107+
digits = digits,
108+
issuer = issuer,
109+
accountName = label,
110+
favorite = favorite,
111+
)
112+
}
113+
}

0 commit comments

Comments
 (0)