Import from andOtp

This commit is contained in:
Rafał Kobyłko 2023-09-30 14:10:58 +02:00
parent b2daccce39
commit d5763471f7
17 changed files with 153 additions and 23 deletions

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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 = {

View File

@ -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
}
}

View File

@ -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 ->

View File

@ -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