mirror of
https://github.com/twofas/2fas-android.git
synced 2025-01-05 14:05:30 +01:00
Import from andOtp
This commit is contained in:
parent
b2daccce39
commit
d5763471f7
@ -9,9 +9,9 @@ data class OtpAuthLink(
|
||||
val link: String?,
|
||||
) {
|
||||
companion object {
|
||||
const val DIGITS_PARAM = "digits"
|
||||
const val PERIOD_PARAM = "period"
|
||||
const val ALGORITHM_PARAM = "algorithm"
|
||||
const val COUNTER = "counter"
|
||||
const val ParamDigits = "digits"
|
||||
const val ParamPeriod = "period"
|
||||
const val ParamAlgorithm = "algorithm"
|
||||
const val ParamCounter = "counter"
|
||||
}
|
||||
}
|
BIN
core/designsystem/src/main/res/drawable/logo_andotp.png
Normal file
BIN
core/designsystem/src/main/res/drawable/logo_andotp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
@ -82,11 +82,13 @@ class Strings(c: Context) {
|
||||
val externalImportRaivo = c.getString(R.string.externalimport_raivo)
|
||||
val externalImportLastPass = c.getString(R.string.externalimport_lastpass)
|
||||
val externalImportAuthenticatorPro = c.getString(R.string.externalimport__authenticatorpro)
|
||||
val externalImportAndOtp = c.getString(R.string.externalimport_andotp)
|
||||
val externalImportGoogleAuthenticatorMsg = c.getString(R.string.introduction__google_authenticator_import_process)
|
||||
val externalImportAegisMsg = c.getString(R.string.externalimport__aegis_msg)
|
||||
val externalImportRaivoMsg = c.getString(R.string.externalimport__raivo_msg)
|
||||
val externalImportLastPassMsg = c.getString(R.string.externalimport__lastpass_msg)
|
||||
val externalImportAuthenticatorProMsg = c.getString(R.string.externalimport__authenticatorpro_msg)
|
||||
val externalImportAndOtpMsg = c.getString(R.string.externalimport__andotp_msg)
|
||||
val externalImportChooseJsonCta = c.getString(R.string.externalimport__choose_json_cta)
|
||||
val externalImportChooseTxtCta = c.getString(R.string.externalimport__choose_txt_cta)
|
||||
val externalImportChooseQrCta = c.getString(R.string.introduction__choose_qr_code)
|
||||
@ -96,11 +98,13 @@ class Strings(c: Context) {
|
||||
val externalImportResultRaivoTitle = c.getString(R.string.externalimport__raivo_title)
|
||||
val externalImportResultLastPassTitle = c.getString(R.string.externalimport__lastpass_title)
|
||||
val externalImportResultAuthenticatorProTitle = c.getString(R.string.externalimport__authenticatorpro_title)
|
||||
val externalImportResultAndOtpTitle = c.getString(R.string.externalimport__andotp_title)
|
||||
val externalImportResultSuccessGoogleAuthenticatorMsg = c.getString(R.string.externalimport__ga_success_msg)
|
||||
val externalImportResultSuccessAegisMsg = c.getString(R.string.externalimport__aegis_success_msg)
|
||||
val externalImportResultSuccessRaivoMsg = c.getString(R.string.externalimport__raivo_success_msg)
|
||||
val externalImportResultSuccessLastPassMsg = c.getString(R.string.externalimport__lastpass_success_msg)
|
||||
val externalImportResultSuccessAuthenticatorProMsg = c.getString(R.string.externalimport__authenticatorpro_success_msg)
|
||||
val externalImportResultSuccessAndOtpMsg = c.getString(R.string.externalimport__andotp_success_msg)
|
||||
val externalImportResultTokensCount = c.getString(R.string.tokens__google_auth_out_of_title)
|
||||
val externalImportResultTokensMsg = c.getString(R.string.tokens__google_auth_import_subtitle_end)
|
||||
val externalImportResultErrorMsg = c.getString(R.string.externalimport__read_error)
|
||||
|
@ -228,9 +228,9 @@ internal class ServicesRepositoryImpl(
|
||||
|
||||
override fun isServiceValid(link: OtpAuthLink): Boolean {
|
||||
return try {
|
||||
val otpAlgorithm = link.params[OtpAuthLink.ALGORITHM_PARAM]
|
||||
val otpDigits = link.params[OtpAuthLink.DIGITS_PARAM]?.toIntOrNull()
|
||||
val otpPeriod = link.params[OtpAuthLink.PERIOD_PARAM]?.toIntOrNull()
|
||||
val otpAlgorithm = link.params[OtpAuthLink.ParamAlgorithm]
|
||||
val otpDigits = link.params[OtpAuthLink.ParamDigits]?.toIntOrNull()
|
||||
val otpPeriod = link.params[OtpAuthLink.ParamPeriod]?.toIntOrNull()
|
||||
|
||||
val algorithm = when {
|
||||
otpAlgorithm == null -> Service.Algorithm.SHA1
|
||||
|
@ -28,25 +28,25 @@ object ServiceParser {
|
||||
}
|
||||
|
||||
val digits = try {
|
||||
link.params[OtpAuthLink.DIGITS_PARAM]?.toInt()
|
||||
link.params[OtpAuthLink.ParamDigits]?.toInt()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val period = try {
|
||||
link.params[OtpAuthLink.PERIOD_PARAM]?.toInt()
|
||||
link.params[OtpAuthLink.ParamPeriod]?.toInt()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val algorithm = try {
|
||||
link.params[OtpAuthLink.ALGORITHM_PARAM]
|
||||
link.params[OtpAuthLink.ParamAlgorithm]
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val counter = try {
|
||||
link.params[OtpAuthLink.COUNTER]?.toInt()
|
||||
link.params[OtpAuthLink.ParamCounter]?.toInt()
|
||||
} catch (e: Exception) {
|
||||
if (link.type.equals("hotp", true)) {
|
||||
1
|
||||
|
@ -2,6 +2,7 @@ package com.twofasapp.feature.externalimport.di
|
||||
|
||||
import com.twofasapp.common.di.KoinModule
|
||||
import com.twofasapp.feature.externalimport.domain.AegisImporter
|
||||
import com.twofasapp.feature.externalimport.domain.AndOtpImporter
|
||||
import com.twofasapp.feature.externalimport.domain.AuthenticatorProImporter
|
||||
import com.twofasapp.feature.externalimport.domain.GoogleAuthenticatorImporter
|
||||
import com.twofasapp.feature.externalimport.domain.LastPassImporter
|
||||
@ -25,5 +26,6 @@ class ExternalImportModule : KoinModule {
|
||||
factoryOf(::RaivoImporter)
|
||||
factoryOf(::LastPassImporter)
|
||||
factoryOf(::AuthenticatorProImporter)
|
||||
factoryOf(::AndOtpImporter)
|
||||
}
|
||||
}
|
@ -113,10 +113,10 @@ internal class AegisImporter(
|
||||
private fun parseParams(entry: Entry): Map<String, String> {
|
||||
val params = mutableMapOf<String, String>()
|
||||
|
||||
entry.info.algo?.let { params[OtpAuthLink.ALGORITHM_PARAM] = it }
|
||||
entry.info.period?.let { params[OtpAuthLink.PERIOD_PARAM] = it.toString() }
|
||||
entry.info.digits?.let { params[OtpAuthLink.DIGITS_PARAM] = it.toString() }
|
||||
entry.info.counter?.let { params[OtpAuthLink.COUNTER] = it.toString() }
|
||||
entry.info.algo?.let { params[OtpAuthLink.ParamAlgorithm] = it }
|
||||
entry.info.period?.let { params[OtpAuthLink.ParamPeriod] = it.toString() }
|
||||
entry.info.digits?.let { params[OtpAuthLink.ParamDigits] = it.toString() }
|
||||
entry.info.counter?.let { params[OtpAuthLink.ParamCounter] = it.toString() }
|
||||
|
||||
return params
|
||||
}
|
||||
|
@ -0,0 +1,105 @@
|
||||
package com.twofasapp.feature.externalimport.domain
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.twofasapp.common.domain.OtpAuthLink
|
||||
import com.twofasapp.common.domain.Service
|
||||
import com.twofasapp.data.services.ServicesRepository
|
||||
import com.twofasapp.data.services.otp.ServiceParser
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.BufferedReader
|
||||
|
||||
internal class AndOtpImporter(
|
||||
private val context: Context,
|
||||
private val jsonSerializer: Json,
|
||||
private val servicesRepository: ServicesRepository,
|
||||
) : ExternalImporter {
|
||||
|
||||
|
||||
@Serializable
|
||||
data class Model(
|
||||
val secret: String,
|
||||
val issuer: String?,
|
||||
val label: String?,
|
||||
val digits: Int?,
|
||||
val period: Int?,
|
||||
val counter: Int?,
|
||||
val type: String?,
|
||||
val algorithm: String?,
|
||||
)
|
||||
|
||||
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 models = jsonSerializer.decodeFromString<List<Model>>(json)
|
||||
|
||||
fileDescriptor?.close()
|
||||
inputStream.close()
|
||||
|
||||
val totalServices = models.size
|
||||
val servicesToImport = mutableListOf<Service?>()
|
||||
|
||||
models
|
||||
.filter { it.digits == 6 || it.digits == 7 || it.digits == 8 || it.digits == null }
|
||||
.filter { it.period == 30 || it.period == 60 || it.period == 90 || it.period == null }
|
||||
.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) ||
|
||||
it.algorithm == null
|
||||
}
|
||||
.filter { servicesRepository.isSecretValid(it.secret) }
|
||||
.forEach { entry ->
|
||||
servicesToImport.add(parseService(entry))
|
||||
}
|
||||
|
||||
return ExternalImport.Success(
|
||||
servicesToImport = servicesToImport.filterNotNull(),
|
||||
totalServicesCount = totalServices,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
return ExternalImport.ParsingError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseService(model: Model): Service? {
|
||||
val otpLink = OtpAuthLink(
|
||||
type = model.type ?: "TOTP",
|
||||
label = model.label ?: "",
|
||||
secret = model.secret,
|
||||
issuer = model.issuer,
|
||||
params = parseParams(model),
|
||||
link = null,
|
||||
)
|
||||
|
||||
return ServiceParser.parseService(otpLink)
|
||||
}
|
||||
|
||||
private fun parseParams(model: Model): Map<String, String> {
|
||||
val params = mutableMapOf<String, String>()
|
||||
model.algorithm?.let { params[OtpAuthLink.ParamAlgorithm] = it }
|
||||
model.counter?.let { params[OtpAuthLink.ParamPeriod] = it.toString() }
|
||||
model.digits?.let { params[OtpAuthLink.ParamDigits] = it.toString() }
|
||||
model.counter?.let { params[OtpAuthLink.ParamCounter] = it.toString() }
|
||||
return params
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ enum class ImportType {
|
||||
Raivo,
|
||||
LastPass,
|
||||
AuthenticatorPro,
|
||||
AndOtp,
|
||||
;
|
||||
}
|
||||
|
||||
@ -18,4 +19,5 @@ val ImportType.image: Int
|
||||
ImportType.Raivo -> R.drawable.ic_import_raivo
|
||||
ImportType.LastPass -> R.drawable.ic_import_lastpass
|
||||
ImportType.AuthenticatorPro -> R.drawable.ic_import_authenticatorpro
|
||||
ImportType.AndOtp -> R.drawable.ic_import_andotp
|
||||
}
|
@ -91,9 +91,9 @@ internal class LastPassImporter(
|
||||
|
||||
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() }
|
||||
account.algorithm.let { params[OtpAuthLink.ParamAlgorithm] = it }
|
||||
account.timeStep.let { params[OtpAuthLink.ParamPeriod] = it.toString() }
|
||||
account.digits.let { params[OtpAuthLink.ParamDigits] = it.toString() }
|
||||
return params
|
||||
}
|
||||
}
|
@ -94,10 +94,10 @@ internal class RaivoImporter(
|
||||
private fun parseParams(entry: Entry): Map<String, String> {
|
||||
val params = mutableMapOf<String, String>()
|
||||
|
||||
entry.algorithm?.let { params[OtpAuthLink.ALGORITHM_PARAM] = it }
|
||||
entry.timer?.let { params[OtpAuthLink.PERIOD_PARAM] = it }
|
||||
entry.digits?.let { params[OtpAuthLink.DIGITS_PARAM] = it }
|
||||
entry.counter?.let { params[OtpAuthLink.COUNTER] = it }
|
||||
entry.algorithm?.let { params[OtpAuthLink.ParamAlgorithm] = it }
|
||||
entry.timer?.let { params[OtpAuthLink.ParamPeriod] = it }
|
||||
entry.digits?.let { params[OtpAuthLink.ParamDigits] = it }
|
||||
entry.counter?.let { params[OtpAuthLink.ParamCounter] = it }
|
||||
|
||||
return params
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ private fun ScreenContent(
|
||||
ImportType.Raivo -> strings.externalImportRaivo
|
||||
ImportType.LastPass -> strings.externalImportLastPass
|
||||
ImportType.AuthenticatorPro -> strings.externalImportAuthenticatorPro
|
||||
ImportType.AndOtp -> strings.externalImportAndOtp
|
||||
}
|
||||
|
||||
val description = when (uiState.importType) {
|
||||
@ -72,6 +73,7 @@ private fun ScreenContent(
|
||||
ImportType.Raivo -> strings.externalImportRaivoMsg
|
||||
ImportType.LastPass -> strings.externalImportLastPassMsg
|
||||
ImportType.AuthenticatorPro -> strings.externalImportAuthenticatorProMsg
|
||||
ImportType.AndOtp -> strings.externalImportAndOtpMsg
|
||||
}
|
||||
|
||||
val ctaPrimary = when (uiState.importType) {
|
||||
@ -80,6 +82,7 @@ private fun ScreenContent(
|
||||
ImportType.Raivo -> strings.externalImportChooseJsonCta
|
||||
ImportType.LastPass -> strings.externalImportChooseJsonCta
|
||||
ImportType.AuthenticatorPro -> strings.externalImportChooseTxtCta
|
||||
ImportType.AndOtp -> strings.externalImportChooseJsonCta
|
||||
}
|
||||
|
||||
val ctaSecondary = when (uiState.importType) {
|
||||
@ -106,6 +109,7 @@ private fun ScreenContent(
|
||||
ImportType.Raivo -> launcher.launch(arrayOf("application/json"))
|
||||
ImportType.LastPass -> launcher.launch(arrayOf("application/json"))
|
||||
ImportType.AuthenticatorPro -> launcher.launch(arrayOf("text/*"))
|
||||
ImportType.AndOtp -> launcher.launch(arrayOf("application/json"))
|
||||
}
|
||||
},
|
||||
ctaSecondaryClick = {
|
||||
|
@ -115,6 +115,7 @@ private fun Result(
|
||||
ImportType.Raivo -> strings.externalImportResultRaivoTitle
|
||||
ImportType.LastPass -> strings.externalImportResultLastPassTitle
|
||||
ImportType.AuthenticatorPro -> strings.externalImportResultAuthenticatorProTitle
|
||||
ImportType.AndOtp -> strings.externalImportResultAndOtpTitle
|
||||
}
|
||||
|
||||
val description = when (readResult) {
|
||||
@ -125,6 +126,7 @@ private fun Result(
|
||||
ImportType.Raivo -> strings.externalImportResultSuccessRaivoMsg
|
||||
ImportType.LastPass -> strings.externalImportResultSuccessLastPassMsg
|
||||
ImportType.AuthenticatorPro -> strings.externalImportResultSuccessAuthenticatorProMsg
|
||||
ImportType.AndOtp -> strings.externalImportResultSuccessAndOtpMsg
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,12 +4,13 @@ import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.twofasapp.android.navigation.NavArg
|
||||
import com.twofasapp.android.navigation.getOrThrow
|
||||
import com.twofasapp.android.navigation.getOrNull
|
||||
import com.twofasapp.android.navigation.getOrThrow
|
||||
import com.twofasapp.common.ktx.decodeBase64
|
||||
import com.twofasapp.common.ktx.launchScoped
|
||||
import com.twofasapp.data.services.ServicesRepository
|
||||
import com.twofasapp.feature.externalimport.domain.AegisImporter
|
||||
import com.twofasapp.feature.externalimport.domain.AndOtpImporter
|
||||
import com.twofasapp.feature.externalimport.domain.AuthenticatorProImporter
|
||||
import com.twofasapp.feature.externalimport.domain.ExternalImport
|
||||
import com.twofasapp.feature.externalimport.domain.GoogleAuthenticatorImporter
|
||||
@ -29,6 +30,7 @@ internal class ExternalImportResultViewModel(
|
||||
private val raivoImporter: RaivoImporter,
|
||||
private val lastPassImporter: LastPassImporter,
|
||||
private val authenticatorProImporter: AuthenticatorProImporter,
|
||||
private val andOtpImporter: AndOtpImporter,
|
||||
) : ViewModel() {
|
||||
|
||||
private val importType: ImportType = enumValueOf(savedStateHandle.getOrThrow(NavArg.ImportType.name))
|
||||
@ -66,6 +68,7 @@ internal class ExternalImportResultViewModel(
|
||||
ImportType.Raivo -> raivoImporter.read(importFileUri.orEmpty().decodeBase64())
|
||||
ImportType.LastPass -> lastPassImporter.read(importFileUri.orEmpty().decodeBase64())
|
||||
ImportType.AuthenticatorPro -> authenticatorProImporter.read(importFileUri.orEmpty().decodeBase64())
|
||||
ImportType.AndOtp -> andOtpImporter.read(importFileUri.orEmpty().decodeBase64())
|
||||
}
|
||||
|
||||
uiState.update { state ->
|
||||
|
@ -72,6 +72,14 @@ internal fun ExternalImportSelectorScreen(
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SettingsLink(
|
||||
title = TwLocale.strings.externalImportAndOtp,
|
||||
image = painterResource(id = R.drawable.logo_andotp),
|
||||
onClick = { onImportTypeSelected(ImportType.AndOtp) }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SettingsDescription(text = TwLocale.strings.externalImportNotice)
|
||||
}
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Loading…
Reference in New Issue
Block a user