mirror of
https://github.com/twofas/2fas-android.git
synced 2025-01-05 14:05:30 +01:00
Import from LastPass
This commit is contained in:
parent
2a13d95833
commit
ebb29e4579
@ -0,0 +1,8 @@
|
||||
<vector android:height="128dp" android:viewportHeight="512"
|
||||
android:viewportWidth="512" android:width="128dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#d32d27" android:pathData="M76.8,0L435.2,0A76.8,76.8 0,0 1,512 76.8L512,435.2A76.8,76.8 0,0 1,435.2 512L76.8,512A76.8,76.8 0,0 1,0 435.2L0,76.8A76.8,76.8 0,0 1,76.8 0z"/>
|
||||
<path android:fillColor="#fff" android:pathData="M108,256m-44,0a44,44 0,1 1,88 0a44,44 0,1 1,-88 0"/>
|
||||
<path android:fillColor="#fff" android:pathData="M227,256m-44,0a44,44 0,1 1,88 0a44,44 0,1 1,-88 0"/>
|
||||
<path android:fillColor="#fff" android:pathData="M347,256m-44,0a44,44 0,1 1,88 0a44,44 0,1 1,-88 0"/>
|
||||
<path android:fillColor="#fff" android:pathData="M438,160L438,160A10,10.24 0,0 1,448 170.24L448,341.76A10,10.24 0,0 1,438 352L438,352A10,10.24 0,0 1,428 341.76L428,170.24A10,10.24 0,0 1,438 160z"/>
|
||||
</vector>
|
@ -73,6 +73,7 @@ class Strings(c: Context) {
|
||||
val externalImportGoogleAuthenticator = c.getString(R.string.externalimport_google_authenticator)
|
||||
val externalImportAegis = c.getString(R.string.externalimport_aegis)
|
||||
val externalImportRaivo = c.getString(R.string.externalimport_raivo)
|
||||
val externalImportLastPass = c.getString(R.string.externalimport_lastpass)
|
||||
val externalImportNotice = c.getString(R.string.externalimport_description)
|
||||
|
||||
val trashTitle = c.getString(R.string.settings__trash)
|
||||
|
@ -3,6 +3,7 @@ package com.twofasapp.feature.externalimport.di
|
||||
import com.twofasapp.di.KoinModule
|
||||
import com.twofasapp.feature.externalimport.domain.AegisImporter
|
||||
import com.twofasapp.feature.externalimport.domain.GoogleAuthenticatorImporter
|
||||
import com.twofasapp.feature.externalimport.domain.LastPassImporter
|
||||
import com.twofasapp.feature.externalimport.domain.RaivoImporter
|
||||
import com.twofasapp.feature.externalimport.ui.result.ImportResultViewModel
|
||||
import com.twofasapp.feature.externalimport.ui.scan.ImportScanViewModel
|
||||
@ -19,5 +20,6 @@ class ExternalImportModule : KoinModule {
|
||||
factoryOf(::GoogleAuthenticatorImporter)
|
||||
factoryOf(::AegisImporter)
|
||||
factoryOf(::RaivoImporter)
|
||||
factoryOf(::LastPassImporter)
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package com.twofasapp.feature.externalimport.domain
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.twofasapp.data.services.ServicesRepository
|
||||
import com.twofasapp.parsers.domain.OtpAuthLink
|
||||
import com.twofasapp.prefs.model.ServiceDto
|
||||
import com.twofasapp.serialization.JsonSerializer
|
||||
import com.twofasapp.services.domain.ConvertOtpLinkToService
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.BufferedReader
|
||||
|
||||
internal class LastPassImporter(
|
||||
private val context: Context,
|
||||
private val jsonSerializer: JsonSerializer,
|
||||
private val convertOtpLinkToService: ConvertOtpLinkToService,
|
||||
private val servicesRepository: ServicesRepository,
|
||||
) : ExternalImporter {
|
||||
|
||||
@Serializable
|
||||
data class Model(
|
||||
val accounts: List<Account>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Account(
|
||||
val issuerName: String,
|
||||
val userName: String,
|
||||
val secret: String,
|
||||
val algorithm: String,
|
||||
val timeStep: Int,
|
||||
val digits: Int,
|
||||
)
|
||||
|
||||
override fun isSchemaSupported(content: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun read(content: String): ExternalImport {
|
||||
try {
|
||||
|
||||
val fileUri = Uri.parse(content)
|
||||
val fileDescriptor = context.contentResolver.openAssetFileDescriptor(fileUri, "r")
|
||||
val size = fileDescriptor?.length ?: 0
|
||||
|
||||
if (size > 10 * 1024 * 1024) {
|
||||
return ExternalImport.ParsingError(RuntimeException("File too big"))
|
||||
}
|
||||
|
||||
val inputStream = context.contentResolver.openInputStream(fileUri)!!
|
||||
val json = inputStream.bufferedReader(Charsets.UTF_8).use(BufferedReader::readText)
|
||||
val model = jsonSerializer.deserialize<Model>(json)
|
||||
|
||||
fileDescriptor?.close()
|
||||
inputStream.close()
|
||||
|
||||
val totalServices = model.accounts.size
|
||||
val servicesToImport = mutableListOf<ServiceDto>()
|
||||
|
||||
model
|
||||
.accounts
|
||||
.filter { it.digits == 6 || it.digits == 7 || it.digits == 8 }
|
||||
.filter { it.timeStep == 30 || it.timeStep == 60 || it.timeStep == 90 }
|
||||
.filter {
|
||||
it.algorithm.equals("SHA1", true) || it.algorithm.equals("SHA224", true) || it.algorithm.equals(
|
||||
"SHA256",
|
||||
true
|
||||
) || it.algorithm.equals("SHA384", true) || it.algorithm.equals("SHA512", true)
|
||||
}
|
||||
.filter { servicesRepository.isSecretValid(it.secret) }
|
||||
.forEach { entry ->
|
||||
servicesToImport.add(parseService(entry))
|
||||
}
|
||||
|
||||
return ExternalImport.Success(
|
||||
servicesToImport = servicesToImport,
|
||||
totalServicesCount = totalServices,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
return ExternalImport.ParsingError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseService(account: Account): ServiceDto {
|
||||
val otpLink = OtpAuthLink(
|
||||
type = "TOTP",
|
||||
label = account.userName,
|
||||
secret = account.secret,
|
||||
issuer = account.issuerName,
|
||||
params = parseParams(account),
|
||||
link = null,
|
||||
)
|
||||
|
||||
val parsed = convertOtpLinkToService.execute(otpLink)
|
||||
|
||||
return parsed.copy(otpAccount = account.userName)
|
||||
}
|
||||
|
||||
private fun parseParams(account: Account): Map<String, String> {
|
||||
val params = mutableMapOf<String, String>()
|
||||
account.algorithm.let { params[OtpAuthLink.ALGORITHM_PARAM] = it }
|
||||
account.timeStep.let { params[OtpAuthLink.PERIOD_PARAM] = it.toString() }
|
||||
account.digits.let { params[OtpAuthLink.DIGITS_PARAM] = it.toString() }
|
||||
return params
|
||||
}
|
||||
}
|
@ -2,8 +2,6 @@ package com.twofasapp.feature.externalimport.domain
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.twofasapp.core.analytics.AnalyticsService
|
||||
import com.twofasapp.parsers.domain.OtpAuthLink
|
||||
import com.twofasapp.prefs.model.ServiceDto
|
||||
import com.twofasapp.serialization.JsonSerializer
|
||||
import com.twofasapp.services.domain.ConvertOtpLinkToService
|
||||
@ -14,7 +12,6 @@ internal class RaivoImporter(
|
||||
private val context: Context,
|
||||
private val jsonSerializer: JsonSerializer,
|
||||
private val convertOtpLinkToService: ConvertOtpLinkToService,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : ExternalImporter {
|
||||
|
||||
@Serializable
|
||||
@ -74,7 +71,7 @@ internal class RaivoImporter(
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
|
||||
return ExternalImport.ParsingError(e)
|
||||
}
|
||||
}
|
||||
|
@ -11,22 +11,24 @@ import com.twofasapp.android.navigation.NavNode
|
||||
import com.twofasapp.android.navigation.withArg
|
||||
import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.Aegis
|
||||
import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.GoogleAuthenticator
|
||||
import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.LastPass
|
||||
import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.Raivo
|
||||
import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.Result
|
||||
import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.Scan
|
||||
import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.Selector
|
||||
import com.twofasapp.feature.externalimport.ui.aegis.AegisRoute
|
||||
import com.twofasapp.feature.externalimport.ui.googleauthenticator.GoogleAuthenticatorRoute
|
||||
import com.twofasapp.feature.externalimport.ui.lastpass.LastPassRoute
|
||||
import com.twofasapp.feature.externalimport.ui.raivo.RaivoRoute
|
||||
import com.twofasapp.feature.externalimport.ui.result.ImportResultRoute
|
||||
import com.twofasapp.feature.externalimport.ui.scan.ImportScanRoute
|
||||
import com.twofasapp.feature.externalimport.ui.selector.SelectorRoute
|
||||
|
||||
object ExternalImportGraph : com.twofasapp.android.navigation.NavGraph {
|
||||
object ExternalImportGraph : NavGraph {
|
||||
override val route: String = "externalimport"
|
||||
}
|
||||
|
||||
enum class ImportType { GoogleAuthenticator, Aegis, Raivo }
|
||||
enum class ImportType { GoogleAuthenticator, Aegis, Raivo, LastPass }
|
||||
|
||||
private object NavArg {
|
||||
val ImportType = navArgument("importType") { type = NavType.StringType; }
|
||||
@ -34,13 +36,14 @@ private object NavArg {
|
||||
val StartFromGallery = navArgument("startFromGallery") { type = NavType.BoolType; }
|
||||
}
|
||||
|
||||
private sealed class ExternalImportNode(override val path: String) : com.twofasapp.android.navigation.NavNode {
|
||||
override val graph: com.twofasapp.android.navigation.NavGraph = ExternalImportGraph
|
||||
private sealed class ExternalImportNode(override val path: String) : NavNode {
|
||||
override val graph: NavGraph = ExternalImportGraph
|
||||
|
||||
object Selector : ExternalImportNode("selector")
|
||||
object GoogleAuthenticator : ExternalImportNode("googleauthenticator")
|
||||
object Aegis : ExternalImportNode("aegis")
|
||||
object Raivo : ExternalImportNode("raivo")
|
||||
object LastPass : ExternalImportNode("lastpass")
|
||||
object Scan : ExternalImportNode("scan?startFromGallery={${NavArg.StartFromGallery.name}}")
|
||||
object Result : ExternalImportNode("result/{${NavArg.ImportType.name}}/{${NavArg.ImportContent.name}}")
|
||||
}
|
||||
@ -59,6 +62,7 @@ fun NavGraphBuilder.externalImportNavigation(
|
||||
onGoogleAuthenticatorClick = { navController.navigate(GoogleAuthenticator.route) },
|
||||
onAegisClick = { navController.navigate(Aegis.route) },
|
||||
onRaivoClick = { navController.navigate(Raivo.route) },
|
||||
onLastPassClick = { navController.navigate(LastPass.route) },
|
||||
)
|
||||
}
|
||||
|
||||
@ -92,6 +96,16 @@ fun NavGraphBuilder.externalImportNavigation(
|
||||
})
|
||||
}
|
||||
|
||||
composable(route = LastPass.route) {
|
||||
LastPassRoute(onFilePicked = { content ->
|
||||
navController.navigate(
|
||||
Result.route
|
||||
.withArg(NavArg.ImportType, ImportType.LastPass.name)
|
||||
.withArg(NavArg.ImportContent, content)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
composable(
|
||||
route = Scan.route,
|
||||
arguments = listOf(NavArg.StartFromGallery)
|
||||
|
@ -44,7 +44,7 @@ internal fun ImportFileScaffold(
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding)
|
||||
.padding(top = padding.calculateTopPadding())
|
||||
) {
|
||||
|
||||
Column(
|
||||
|
@ -50,7 +50,7 @@ internal fun GoogleAuthenticatorRoute(
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding)
|
||||
.padding(top = padding.calculateTopPadding())
|
||||
) {
|
||||
|
||||
Column(
|
||||
|
@ -0,0 +1,28 @@
|
||||
package com.twofasapp.feature.externalimport.ui.lastpass
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.twofasapp.core.encoding.encodeBase64ToString
|
||||
import com.twofasapp.feature.externalimport.ui.common.ImportDescription
|
||||
import com.twofasapp.feature.externalimport.ui.common.ImportFilePickerButton
|
||||
import com.twofasapp.feature.externalimport.ui.common.ImportFileScaffold
|
||||
import com.twofasapp.resources.R
|
||||
|
||||
@Composable
|
||||
internal fun LastPassRoute(
|
||||
onFilePicked: (String) -> Unit,
|
||||
) {
|
||||
|
||||
ImportFileScaffold(
|
||||
title = stringResource(id = R.string.externalimport_lastpass),
|
||||
image = painterResource(id = R.drawable.ic_import_lastpass),
|
||||
description = { ImportDescription(text = stringResource(id = R.string.externalimport__lastpass_msg)) }
|
||||
) {
|
||||
ImportFilePickerButton(
|
||||
text = stringResource(id = R.string.externalimport__choose_json_cta),
|
||||
fileType = "application/json",
|
||||
onFilePicked = { onFilePicked(it.toString().encodeBase64ToString()) }
|
||||
)
|
||||
}
|
||||
}
|
@ -60,7 +60,7 @@ internal fun ImportResultRoute(
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding)
|
||||
.padding(top = padding.calculateTopPadding())
|
||||
) {
|
||||
|
||||
Column(
|
||||
@ -78,6 +78,7 @@ internal fun ImportResultRoute(
|
||||
ImportType.GoogleAuthenticator -> R.drawable.ic_import_ga
|
||||
ImportType.Aegis -> R.drawable.ic_import_aegis
|
||||
ImportType.Raivo -> R.drawable.ic_import_raivo
|
||||
ImportType.LastPass -> R.drawable.ic_import_lastpass
|
||||
}
|
||||
),
|
||||
contentDescription = null,
|
||||
|
@ -9,6 +9,7 @@ import com.twofasapp.core.encoding.decodeBase64
|
||||
import com.twofasapp.feature.externalimport.domain.AegisImporter
|
||||
import com.twofasapp.feature.externalimport.domain.ExternalImport
|
||||
import com.twofasapp.feature.externalimport.domain.GoogleAuthenticatorImporter
|
||||
import com.twofasapp.feature.externalimport.domain.LastPassImporter
|
||||
import com.twofasapp.feature.externalimport.domain.RaivoImporter
|
||||
import com.twofasapp.feature.externalimport.navigation.ImportType
|
||||
import com.twofasapp.resources.R
|
||||
@ -28,6 +29,7 @@ internal class ImportResultViewModel(
|
||||
private val googleAuthenticatorImporter: GoogleAuthenticatorImporter,
|
||||
private val aegisImporter: AegisImporter,
|
||||
private val raivoImporter: RaivoImporter,
|
||||
private val lastPassImporter: LastPassImporter,
|
||||
private val addServiceCase: AddServiceCase,
|
||||
private val syncBackupDispatcher: SyncBackupWorkDispatcher,
|
||||
) : BaseViewModel() {
|
||||
@ -41,6 +43,7 @@ internal class ImportResultViewModel(
|
||||
ImportType.GoogleAuthenticator -> googleAuthenticatorImporter.read(content.decodeBase64())
|
||||
ImportType.Aegis -> aegisImporter.read(content.decodeBase64())
|
||||
ImportType.Raivo -> raivoImporter.read(content.decodeBase64())
|
||||
ImportType.LastPass -> lastPassImporter.read(content.decodeBase64())
|
||||
}
|
||||
|
||||
when (result) {
|
||||
@ -95,11 +98,13 @@ internal class ImportResultViewModel(
|
||||
ImportType.GoogleAuthenticator -> R.string.externalimport__ga_title
|
||||
ImportType.Aegis -> R.string.externalimport__aegis_title
|
||||
ImportType.Raivo -> R.string.externalimport__raivo_title
|
||||
ImportType.LastPass -> R.string.externalimport__lastpass_title
|
||||
}
|
||||
|
||||
private fun getSuccessDescription(type: ImportType) = when (type) {
|
||||
ImportType.GoogleAuthenticator -> R.string.externalimport__ga_success_msg
|
||||
ImportType.Aegis -> R.string.externalimport__aegis_success_msg
|
||||
ImportType.Raivo -> R.string.externalimport__raivo_success_msg
|
||||
ImportType.LastPass -> R.string.externalimport__lastpass_success_msg
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ internal fun ImportScanRoute(
|
||||
topBar = { TwTopAppBar(stringResource(id = com.twofasapp.resources.R.string.commons__scan_qr_code)) }
|
||||
) { padding ->
|
||||
|
||||
QrScannerScreen(isGalleryEnabled = true, startWithGallery = startFromGallery, modifier = Modifier.padding(padding))
|
||||
QrScannerScreen(isGalleryEnabled = true, startWithGallery = startFromGallery, modifier = Modifier.padding(top = padding.calculateTopPadding()))
|
||||
|
||||
LaunchedEffect(uiState.value.isSuccess) {
|
||||
if (uiState.value.isSuccess) {
|
||||
|
@ -18,11 +18,13 @@ internal fun SelectorRoute(
|
||||
onGoogleAuthenticatorClick: () -> Unit,
|
||||
onAegisClick: () -> Unit,
|
||||
onRaivoClick: () -> Unit,
|
||||
onLastPassClick: () -> Unit,
|
||||
) {
|
||||
SelectorScreen(
|
||||
onGoogleAuthenticatorClick = onGoogleAuthenticatorClick,
|
||||
onAegisClick = onAegisClick,
|
||||
onRaivoClick = onRaivoClick
|
||||
onRaivoClick = onRaivoClick,
|
||||
onLastPassClick = onLastPassClick,
|
||||
)
|
||||
}
|
||||
|
||||
@ -31,13 +33,14 @@ private fun SelectorScreen(
|
||||
onGoogleAuthenticatorClick: () -> Unit,
|
||||
onAegisClick: () -> Unit,
|
||||
onRaivoClick: () -> Unit,
|
||||
onLastPassClick: () -> Unit,
|
||||
) {
|
||||
|
||||
Scaffold(
|
||||
topBar = { TwTopAppBar(TwLocale.strings.externalImportTitle) }
|
||||
) { padding ->
|
||||
|
||||
LazyColumn(modifier = Modifier.padding(padding)) {
|
||||
LazyColumn(modifier = Modifier.padding(top = padding.calculateTopPadding())) {
|
||||
item {
|
||||
SettingsHeader(title = TwLocale.strings.externalImportHeader)
|
||||
}
|
||||
@ -66,6 +69,14 @@ private fun SelectorScreen(
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SettingsLink(
|
||||
title = TwLocale.strings.externalImportLastPass,
|
||||
image = painterResource(id = R.drawable.logo_lastpass),
|
||||
onClick = onLastPassClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SettingsDescription(text = TwLocale.strings.externalImportNotice)
|
||||
}
|
||||
|
BIN
resources/src/main/res/drawable-night/ic_import_lastpass.png
Normal file
BIN
resources/src/main/res/drawable-night/ic_import_lastpass.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
BIN
resources/src/main/res/drawable/ic_import_lastpass.png
Normal file
BIN
resources/src/main/res/drawable/ic_import_lastpass.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
Loading…
Reference in New Issue
Block a user