0
0
mirror of https://github.com/markusfisch/BinaryEye.git synced 2024-09-20 12:02:17 +02:00

Remove parsing of OTPs

We should not check the content of the OTP code, instead let the receiver handle the
content and give appropriate errors, if needed.
This commit is contained in:
molikuner 2019-09-30 16:07:52 +02:00 committed by Markus Fisch
parent d925e136ef
commit adecdf7e27
3 changed files with 3 additions and 230 deletions

View File

@ -1,22 +1,10 @@
package de.markusfisch.android.binaryeye.actions.otpauth
import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.SchemeAction
object OtpauthAction : IntentAction() {
object OtpauthAction : SchemeAction() {
override val iconResId: Int = R.drawable.ic_action_otpauth
override val titleResId: Int = R.string.otpauth_add
override val errorMsg: Int = R.string.otpauth_error
override fun canExecuteOn(data: ByteArray): Boolean {
return OtpauthParser(String(data)) != null
}
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
return OtpauthParser(String(data))?.let {
Intent(Intent.ACTION_VIEW, it.uri)
}
}
override val scheme: String = "otpauth"
}

View File

@ -1,76 +0,0 @@
package de.markusfisch.android.binaryeye.actions.otpauth
import android.net.Uri
class OtpauthParser private constructor(private val match: MatchResult) {
val otpType: String
get() = match.groupValues[1]
private val labelIssuer: String
get() = match.groupValues[2]
val accountName: String
get() = match.groupValues[3].takeIf { it.isNotEmpty() } ?: match.groupValues[4]
val secret: String
get() = match.groupValues[5]
private val parameterIssuer: String
get() = match.groupValues[6]
val algorithm: String
get() = match.groupValues[7].takeIf { it.isNotEmpty() } ?: "SHA1"
val digits: Int
get() = match.groupValues[8].toIntOrNull() ?: 6
val period: Int
get() = match.groupValues[9].toIntOrNull() ?: 30
private val parameterCounter: String
get() = match.groupValues[10]
val issuer: String
get() = labelIssuer.takeIf { it.isNotEmpty() } ?: parameterIssuer
val counter: Int
get() = parameterCounter.toInt()
private val isHotp: Boolean = otpType == "hotp"
val isValid: Boolean =
(labelIssuer.isNotEmpty() == parameterIssuer.isEmpty() || labelIssuer.isEmpty())
&& secret.isNotEmpty() && isHotp == parameterCounter.isNotEmpty()
val uri: Uri
get() = Uri.Builder().apply {
scheme("otpauth")
authority(otpType)
if (issuer.isNotEmpty()) {
encodedPath("/$issuer:$accountName")
appendQueryParameter("issuer", issuer)
} else {
encodedPath("/$accountName")
}
appendQueryParameter("secret", secret)
appendQueryParameter("algorithm", algorithm)
appendQueryParameter("digits", digits.toString())
if (isHotp) {
appendQueryParameter("counter", counter.toString())
} else {
appendQueryParameter("period", period.toString())
}
}.build()
companion object {
private const val issuerAndAccount =
"""(?:(?:([^?:]+)(?::|%3A)(?:%20)*([^?]+))|([^?]+))"""//.toRegex()
/** allowing `padding` even though it should not be there, will be removed in output*/
private const val secret = """(?:secret=([2-7A-Z]+)=*)"""//.toRegex()
/** `\2` references the issuer of [issuerAndAccount] */
private const val issuer = """(?:issuer=\2)|(?:issuer=([^&]+))"""//.toRegex()
private const val algorithm = """(?:algorithm=(SHA(?:1|256|512)))"""//.toRegex()
private const val digits = """(?:digits=([0-9]+))"""//.toRegex()
private const val period = """(?:period=([0-9]+))"""//.toRegex()
private const val counter = """(?:counter=([0-9]+))"""//.toRegex()
private val otpauthRegex =
"""^otpauth://([ht]otp)/$issuerAndAccount\?(?:&?(?:$secret|$issuer|$algorithm|$digits|$period|$counter))+$""".toRegex(
RegexOption.IGNORE_CASE
)
operator fun invoke(input: String): OtpauthParser? {
return otpauthRegex.matchEntire(input)?.let(::OtpauthParser)?.takeIf { it.isValid }
}
}
}

View File

@ -1,139 +0,0 @@
package de.markusfisch.android.binaryeye.actions.otpauth
import de.markusfisch.android.binaryeye.assertThrows
import de.markusfisch.android.binaryeye.simpleFail
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
import org.junit.Test
class OtpauthParserTest {
@Test
fun noOtpauthUri() {
assertNull(OtpauthParser("random string"))
}
@Test
fun missingSecret() {
assertNull(OtpauthParser("otpauth://totp/issuerName:accountName"))
}
@Test
fun missingCounterHotp() {
assertNull(OtpauthParser("otpauth://hotp/accountName?secret=secretString"))
}
@Test
fun minimalHotp() {
val parser = validParser("otpauth://hotp/accountName?secret=secretString&counter=50")
assertEquals("hotp", parser.otpType)
assertEquals("accountName", parser.accountName)
assertEquals("secretString", parser.secret)
assertEquals("SHA1", parser.algorithm)
assertEquals(6, parser.digits)
assertEquals(30, parser.period)
assertEquals(50, parser.counter)
}
@Test
fun normalHotp() {
val parser = validParser("otpauth://hotp/issuerName:accountName?secret=secretString&counter=0")
assertEquals("hotp", parser.otpType)
assertEquals("issuerName", parser.issuer)
assertEquals("accountName", parser.accountName)
assertEquals("secretString", parser.secret)
assertEquals("SHA1", parser.algorithm)
assertEquals(6, parser.digits)
assertEquals(30, parser.period)
assertEquals(0, parser.counter)
}
@Test
fun missingIssuerParamHotp() {
val parser = validParser("otpauth://hotp/issuerName:accountName?secret=secretString&counter=50&algorithm=SHA256&digits=8")
assertEquals("hotp", parser.otpType)
assertEquals("issuerName", parser.issuer)
assertEquals("accountName", parser.accountName)
assertEquals("secretString", parser.secret)
assertEquals("SHA256", parser.algorithm)
assertEquals(8, parser.digits)
assertEquals(30, parser.period)
assertEquals(50, parser.counter)
}
@Test
fun fullHotp() {
val parser = validParser("otpauth://hotp/issuerName:accountName?secret=secretString&counter=50&issuer=issuerName&algorithm=SHA256&digits=8")
assertEquals("hotp", parser.otpType)
assertEquals("issuerName", parser.issuer)
assertEquals("accountName", parser.accountName)
assertEquals("secretString", parser.secret)
assertEquals("SHA256", parser.algorithm)
assertEquals(8, parser.digits)
assertEquals(30, parser.period)
assertEquals(50, parser.counter)
}
@Test
fun minimalTotp() {
val parser = validParser("otpauth://totp/accountName?secret=secretString")
assertEquals("totp", parser.otpType)
assertEquals("accountName", parser.accountName)
assertEquals("secretString", parser.secret)
assertEquals("SHA1", parser.algorithm)
assertEquals(6, parser.digits)
assertEquals(30, parser.period)
assertThrows<NumberFormatException> {
parser.counter
}
}
@Test
fun normalTotp() {
val parser = validParser("otpauth://totp/issuerName:accountName?secret=secretString")
assertEquals("totp", parser.otpType)
assertEquals("issuerName", parser.issuer)
assertEquals("accountName", parser.accountName)
assertEquals("secretString", parser.secret)
assertEquals("SHA1", parser.algorithm)
assertEquals(6, parser.digits)
assertEquals(30, parser.period)
assertThrows<NumberFormatException> {
parser.counter
}
}
@Test
fun missingIssuerParamTotp() {
val parser = validParser("otpauth://totp/issuerName:accountName?secret=secretString&algorithm=SHA256&digits=8&period=60")
assertEquals("totp", parser.otpType)
assertEquals("issuerName", parser.issuer)
assertEquals("accountName", parser.accountName)
assertEquals("secretString", parser.secret)
assertEquals("SHA256", parser.algorithm)
assertEquals(8, parser.digits)
assertEquals(60, parser.period)
assertThrows<NumberFormatException> {
parser.counter
}
}
@Test
fun fullTotp() {
val parser = validParser("otpauth://totp/issuerName:accountName?secret=secretString&issuer=issuerName&algorithm=SHA256&digits=8&period=60")
assertEquals("totp", parser.otpType)
assertEquals("issuerName", parser.issuer)
assertEquals("accountName", parser.accountName)
assertEquals("secretString", parser.secret)
assertEquals("SHA256", parser.algorithm)
assertEquals(8, parser.digits)
assertEquals(60, parser.period)
assertThrows<NumberFormatException> {
parser.counter
}
}
private fun validParser(input: String): OtpauthParser {
return OtpauthParser(input) ?: simpleFail("Could not get parser of valid input")
}
}