mirror of
https://github.com/bitwarden/android.git
synced 2026-03-25 15:51:22 -05:00
[BWA-14] Support importing LastPass exports (#98)
This commit is contained in:
@@ -18,5 +18,11 @@ enum class AuthenticatorItemAlgorithm {
|
||||
/**
|
||||
* Authenticator item verification code uses SHA512 hash.
|
||||
*/
|
||||
SHA512
|
||||
SHA512,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromStringOrNull(value: String): AuthenticatorItemAlgorithm? =
|
||||
entries.find { it.name.equals(value, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,10 @@ enum class AuthenticatorItemType {
|
||||
* Steam's implementation of a one time password.
|
||||
*/
|
||||
STEAM,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromStringOrNull(value: String): AuthenticatorItemType? =
|
||||
entries.find { it.name.equals(value, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDat
|
||||
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
|
||||
import com.bitwarden.authenticator.data.platform.manager.imports.parsers.BitwardenExportParser
|
||||
import com.bitwarden.authenticator.data.platform.manager.imports.parsers.ExportParser
|
||||
import com.bitwarden.authenticator.data.platform.manager.imports.parsers.LastPassExportParser
|
||||
import com.bitwarden.authenticator.data.platform.manager.imports.parsers.TwoFasExportParser
|
||||
|
||||
/**
|
||||
@@ -22,13 +23,18 @@ class ImportManagerImpl(
|
||||
val parser: ExportParser = when (importFileFormat) {
|
||||
ImportFileFormat.BITWARDEN_JSON -> BitwardenExportParser(importFileFormat)
|
||||
ImportFileFormat.TWO_FAS_JSON -> TwoFasExportParser()
|
||||
ImportFileFormat.LAST_PASS_JSON -> LastPassExportParser()
|
||||
}
|
||||
|
||||
return parser.parse(byteArray)
|
||||
.map { authenticatorDiskSource.saveItem(*it.toTypedArray()) }
|
||||
.fold(
|
||||
onSuccess = { ImportDataResult.Success },
|
||||
onFailure = { ImportDataResult.Error }
|
||||
)
|
||||
return try {
|
||||
parser.parse(byteArray)
|
||||
.mapCatching { authenticatorDiskSource.saveItem(*it.toTypedArray()) }
|
||||
.fold(
|
||||
onSuccess = { ImportDataResult.Success },
|
||||
onFailure = { ImportDataResult.Error }
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
ImportDataResult.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ enum class ImportFileFormat(
|
||||
) {
|
||||
BITWARDEN_JSON("application/json"),
|
||||
TWO_FAS_JSON("*/*"),
|
||||
LAST_PASS_JSON("application/json")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.bitwarden.authenticator.data.platform.manager.imports.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LastPassJsonExport(
|
||||
val deviceId: String,
|
||||
val deviceSecret: String,
|
||||
val localDeviceId: String,
|
||||
val deviceName: String,
|
||||
val version: Int,
|
||||
val accounts: List<Account>,
|
||||
val folders: List<Folder>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Account(
|
||||
@SerialName("accountID")
|
||||
val accountId: String,
|
||||
val issuerName: String,
|
||||
val originalIssuerName: String,
|
||||
val userName: String,
|
||||
val originalUserName: String,
|
||||
val pushNotification: Boolean,
|
||||
val secret: String,
|
||||
val timeStep: Int,
|
||||
val digits: Int,
|
||||
val creationTimestamp: Long,
|
||||
val isFavorite: Boolean,
|
||||
val algorithm: String,
|
||||
val folderData: FolderData?,
|
||||
val backupInfo: BackupInfo?,
|
||||
) {
|
||||
@Serializable
|
||||
data class FolderData(
|
||||
val folderId: String,
|
||||
val position: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BackupInfo(
|
||||
val creationDate: String,
|
||||
val deviceOs: String,
|
||||
val appVersion: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Folder(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val isOpened: Boolean,
|
||||
)
|
||||
}
|
||||
@@ -10,9 +10,11 @@ import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFil
|
||||
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
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
|
||||
class BitwardenExportParser(
|
||||
private val fileFormat: ImportFileFormat,
|
||||
@@ -32,14 +34,22 @@ class BitwardenExportParser(
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
return importJson
|
||||
.decodeFromStream<ExportJsonData>(ByteArrayInputStream(byteArray))
|
||||
.asSuccess()
|
||||
.map { exportData ->
|
||||
exportData
|
||||
.items
|
||||
.toAuthenticatorItemEntities()
|
||||
}
|
||||
return try {
|
||||
importJson
|
||||
.decodeFromStream<ExportJsonData>(ByteArrayInputStream(byteArray))
|
||||
.asSuccess()
|
||||
.mapCatching { exportData ->
|
||||
exportData
|
||||
.items
|
||||
.toAuthenticatorItemEntities()
|
||||
}
|
||||
} catch (e: SerializationException) {
|
||||
e.asFailure()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.asFailure()
|
||||
} catch (e: IOException) {
|
||||
e.asFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<ExportJsonData.ExportItem>.toAuthenticatorItemEntities() =
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
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.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
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* An [ExportParser] responsible for transforming LastPass export files into Bitwarden Authenticator
|
||||
* items.
|
||||
*/
|
||||
class LastPassExportParser : ExportParser {
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
override fun parse(byteArray: ByteArray): Result<List<AuthenticatorItemEntity>> {
|
||||
val importJson = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
return try {
|
||||
importJson
|
||||
.decodeFromStream<LastPassJsonExport>(ByteArrayInputStream(byteArray))
|
||||
.asSuccess()
|
||||
.mapCatching { exportData ->
|
||||
exportData
|
||||
.accounts
|
||||
.toAuthenticatorItemEntities()
|
||||
}
|
||||
} catch (e: SerializationException) {
|
||||
e.asFailure()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.asFailure()
|
||||
} catch (e: IOException) {
|
||||
e.asFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<LastPassJsonExport.Account>.toAuthenticatorItemEntities() =
|
||||
map { it.toAuthenticatorItemEntity() }
|
||||
|
||||
private fun LastPassJsonExport.Account.toAuthenticatorItemEntity(): AuthenticatorItemEntity {
|
||||
|
||||
// Lastpass only supports TOTP codes.
|
||||
val type = AuthenticatorItemType.TOTP
|
||||
|
||||
val algorithmEnum = AuthenticatorItemAlgorithm
|
||||
.fromStringOrNull(algorithm)
|
||||
?: throw IllegalArgumentException("Unsupported algorithm.")
|
||||
|
||||
return AuthenticatorItemEntity(
|
||||
id = UUID.randomUUID().toString(),
|
||||
key = secret,
|
||||
type = type,
|
||||
algorithm = algorithmEnum,
|
||||
period = timeStep,
|
||||
digits = digits,
|
||||
issuer = originalIssuerName,
|
||||
userId = null,
|
||||
accountName = originalUserName,
|
||||
favorite = isFavorite,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,20 @@ 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.TwoFasJsonExport
|
||||
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
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
|
||||
private const val TOKEN_TYPE_HOTP = "HOTP"
|
||||
|
||||
/**
|
||||
* A [ExportParser] responsible for transforming 2FAS export files into Bitwarden Authenticator
|
||||
* An [ExportParser] responsible for transforming 2FAS export files into Bitwarden Authenticator
|
||||
* items.
|
||||
*/
|
||||
class TwoFasExportParser : ExportParser {
|
||||
@@ -30,14 +33,22 @@ class TwoFasExportParser : ExportParser {
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
return importJson
|
||||
.decodeFromStream<TwoFasJsonExport>(ByteArrayInputStream(byteArray))
|
||||
.asSuccess()
|
||||
.map { exportData ->
|
||||
exportData
|
||||
.services
|
||||
.toAuthenticatorItemEntities()
|
||||
}
|
||||
return try {
|
||||
importJson
|
||||
.decodeFromStream<TwoFasJsonExport>(ByteArrayInputStream(byteArray))
|
||||
.asSuccess()
|
||||
.mapCatching { exportData ->
|
||||
exportData
|
||||
.services
|
||||
.toAuthenticatorItemEntities()
|
||||
}
|
||||
} catch (e: SerializationException) {
|
||||
e.asFailure()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.asFailure()
|
||||
} catch (e: IOException) {
|
||||
e.asFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<TwoFasJsonExport.Service>.toAuthenticatorItemEntities() =
|
||||
@@ -51,11 +62,7 @@ class TwoFasExportParser : ExportParser {
|
||||
if (tokenType.equals(other = TOKEN_TYPE_HOTP, ignoreCase = true)) {
|
||||
null
|
||||
} else {
|
||||
AuthenticatorItemType
|
||||
.entries
|
||||
.find { entry ->
|
||||
entry.name.equals(other = tokenType, ignoreCase = true)
|
||||
}
|
||||
AuthenticatorItemType.fromStringOrNull(tokenType)
|
||||
}
|
||||
}
|
||||
?: throw IllegalArgumentException("Unsupported OTP type: ${otp.tokenType}.")
|
||||
|
||||
@@ -12,4 +12,5 @@ val ImportFileFormat.displayLabel: Text
|
||||
get() = when (this) {
|
||||
ImportFileFormat.BITWARDEN_JSON -> R.string.import_format_label_bitwarden_json.asText()
|
||||
ImportFileFormat.TWO_FAS_JSON -> R.string.import_format_label_2fas_json.asText()
|
||||
ImportFileFormat.LAST_PASS_JSON -> R.string.import_format_label_lastpass_json.asText()
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
<string name="export_format_label_csv">.csv</string>
|
||||
<string name="import_format_label_bitwarden_json">Bitwarden (.json)</string>
|
||||
<string name="import_format_label_2fas_json">2FAS (.2fas)</string>
|
||||
<string name="import_format_label_lastpass_json">LastPass (.json)</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user