-
Notifications
You must be signed in to change notification settings - Fork 796
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add logic for parting a TOTP code from a Uri or Intent (#4032)
- Loading branch information
1 parent
78d1454
commit a5cf4f4
Showing
5 changed files
with
276 additions
and
0 deletions.
There are no files selected for viewing
55 changes: 55 additions & 0 deletions
55
app/src/main/java/com/x8bit/bitwarden/ui/vault/model/TotpData.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package com.x8bit.bitwarden.ui.vault.model | ||
|
||
import android.os.Parcelable | ||
import kotlinx.parcelize.Parcelize | ||
|
||
/** | ||
* Represents the data for TOTP deeplink. | ||
* | ||
* @property uri The raw uri as a string. | ||
* @property issuer The issuer parameter is a string value indicating the provider or service this | ||
* account is associated with, URL-encoded according to RFC 3986. | ||
* @property accountName The users email address. | ||
* @property secret The secret parameter is an arbitrary key value encoded in Base32 according to | ||
* RFC 3548. The padding specified in RFC 3548 section 2.2 is not required and should be omitted. | ||
* @property digits The digits parameter may have the values 6 or 8, and determines how long of a | ||
* one-time passcode to display to the user. | ||
* @property period The period parameter defines a period that a TOTP code will be valid for, in | ||
* seconds. | ||
* @property algorithm The algorithm may have the values. | ||
*/ | ||
@Parcelize | ||
data class TotpData( | ||
val uri: String, | ||
val issuer: String?, | ||
val accountName: String?, | ||
val secret: String, | ||
val digits: Int, | ||
val period: Int, | ||
val algorithm: CryptoHashAlgorithm, | ||
) : Parcelable { | ||
/** | ||
* A representation of the various cryptographic hash algorithms used by TOTP. | ||
*/ | ||
enum class CryptoHashAlgorithm(val value: String) { | ||
SHA_1(value = "sha1"), | ||
SHA_256(value = "sha256"), | ||
SHA_512(value = "sha512"), | ||
MD_5(value = "md5"), | ||
; | ||
|
||
@Suppress("UndocumentedPublicClass") | ||
companion object { | ||
/** | ||
* Attempts to convert the string [value] to a valid [CryptoHashAlgorithm] or null if | ||
* a match could not be found. | ||
*/ | ||
fun parse( | ||
value: String?, | ||
): CryptoHashAlgorithm? = | ||
CryptoHashAlgorithm | ||
.entries | ||
.firstOrNull { it.value.equals(other = value, ignoreCase = true) } | ||
} | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
app/src/main/java/com/x8bit/bitwarden/ui/vault/util/TotpIntentUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.x8bit.bitwarden.ui.vault.util | ||
|
||
import android.content.Intent | ||
import com.x8bit.bitwarden.ui.vault.model.TotpData | ||
|
||
/** | ||
* Checks if the given [Intent] contains data for a TOTP. The [TotpData] will be returned when the | ||
* correct data is present or `null` if data is invalid or missing. | ||
*/ | ||
fun Intent.getTotpDataOrNull(): TotpData? = this.data?.getTotpDataOrNull() |
48 changes: 48 additions & 0 deletions
48
app/src/main/java/com/x8bit/bitwarden/ui/vault/util/TotpUriUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package com.x8bit.bitwarden.ui.vault.util | ||
|
||
import android.net.Uri | ||
import com.x8bit.bitwarden.ui.vault.model.TotpData | ||
|
||
private const val TOTP_HOST_NAME: String = "totp" | ||
private const val TOTP_SCHEME_NAME: String = "otpauth" | ||
private const val PARAM_NAME_ALGORITHM: String = "algorithm" | ||
private const val PARAM_NAME_DIGITS: String = "digits" | ||
private const val PARAM_NAME_ISSUER: String = "issuer" | ||
private const val PARAM_NAME_PERIOD: String = "period" | ||
private const val PARAM_NAME_SECRET: String = "secret" | ||
|
||
/** | ||
* Checks if the given [Uri] contains valid data for a TOTP. The [TotpData] will be returned when | ||
* the correct data is present or `null` if data is invalid or missing. | ||
*/ | ||
fun Uri.getTotpDataOrNull(): TotpData? { | ||
// Must be a "otpauth" scheme | ||
if (!this.scheme.equals(other = TOTP_SCHEME_NAME, ignoreCase = true)) return null | ||
// Must be a "totp" host | ||
if (!this.host.equals(other = TOTP_HOST_NAME, ignoreCase = true)) return null | ||
// Must contain a "secret" | ||
val secret = this.getQueryParameter(PARAM_NAME_SECRET)?.trim() ?: return null | ||
val segments = this.pathSegments?.firstOrNull()?.split(":") | ||
val segmentCount = segments?.size ?: 0 | ||
return TotpData( | ||
uri = this.toString(), | ||
issuer = this.getQueryParameter(PARAM_NAME_ISSUER) | ||
?: segments?.firstOrNull()?.trim()?.takeIf { segmentCount > 1 }, | ||
accountName = if (segmentCount > 1) { | ||
segments?.getOrNull(index = 1)?.trim() | ||
} else { | ||
segments?.firstOrNull()?.trim() | ||
}, | ||
secret = secret, | ||
digits = this.getQueryParameter(PARAM_NAME_DIGITS)?.trim()?.toIntOrNull() ?: 6, | ||
period = this | ||
.getQueryParameter(PARAM_NAME_PERIOD) | ||
?.trim() | ||
?.toIntOrNull() | ||
?.takeUnless { it <= 0 } | ||
?: 30, | ||
algorithm = TotpData.CryptoHashAlgorithm | ||
.parse(value = this.getQueryParameter(PARAM_NAME_ALGORITHM)?.trim()) | ||
?: TotpData.CryptoHashAlgorithm.SHA_1, | ||
) | ||
} |
62 changes: 62 additions & 0 deletions
62
app/src/test/java/com/x8bit/bitwarden/ui/vault/util/TotpIntentUtilsTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package com.x8bit.bitwarden.ui.vault.util | ||
|
||
import android.content.Intent | ||
import android.net.Uri | ||
import com.x8bit.bitwarden.ui.vault.model.TotpData | ||
import io.mockk.every | ||
import io.mockk.mockk | ||
import io.mockk.mockkStatic | ||
import io.mockk.unmockkStatic | ||
import org.junit.jupiter.api.AfterEach | ||
import org.junit.jupiter.api.Assertions.assertEquals | ||
import org.junit.jupiter.api.Assertions.assertNull | ||
import org.junit.jupiter.api.BeforeEach | ||
import org.junit.jupiter.api.Test | ||
|
||
class TotpIntentUtilsTest { | ||
|
||
@BeforeEach | ||
fun setup() { | ||
mockkStatic(Uri::getTotpDataOrNull) | ||
} | ||
|
||
@AfterEach | ||
fun tearDown() { | ||
unmockkStatic(Uri::getTotpDataOrNull) | ||
} | ||
|
||
@Test | ||
fun `getTotpDataOrNull with null data should return null`() { | ||
val intent = mockk<Intent> { | ||
every { data } returns null | ||
} | ||
|
||
assertNull(intent.getTotpDataOrNull()) | ||
} | ||
|
||
@Test | ||
fun `getTotpDataOrNull with null uri getTotpDataOrNull should return null`() { | ||
val uri = mockk<Uri> { | ||
every { getTotpDataOrNull() } returns null | ||
} | ||
val intent = mockk<Intent> { | ||
every { data } returns uri | ||
} | ||
|
||
assertNull(intent.getTotpDataOrNull()) | ||
} | ||
|
||
@Test | ||
fun `getTotpDataOrNull with valid uri getTotpDataOrNull should return totpData`() { | ||
val totpData = mockk<TotpData>() | ||
val uri = mockk<Uri> { | ||
every { getTotpDataOrNull() } returns totpData | ||
} | ||
val intent = mockk<Intent> { | ||
every { data } returns uri | ||
} | ||
println(intent) | ||
|
||
assertEquals(totpData, intent.getTotpDataOrNull()) | ||
} | ||
} |
101 changes: 101 additions & 0 deletions
101
app/src/test/java/com/x8bit/bitwarden/ui/vault/util/TotpUriUtilsTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package com.x8bit.bitwarden.ui.vault.util | ||
|
||
import android.net.Uri | ||
import com.x8bit.bitwarden.ui.vault.model.TotpData | ||
import com.x8bit.bitwarden.ui.vault.model.TotpData.CryptoHashAlgorithm | ||
import io.mockk.every | ||
import io.mockk.mockk | ||
import org.junit.jupiter.api.Assertions.assertEquals | ||
import org.junit.jupiter.api.Assertions.assertNull | ||
import org.junit.jupiter.api.Test | ||
|
||
class TotpUriUtilsTest { | ||
|
||
@Test | ||
fun `getTotpDataOrNull with incorrect scheme returns null`() { | ||
val uri = mockk<Uri> { | ||
every { scheme } returns "wrong" | ||
} | ||
|
||
assertNull(uri.getTotpDataOrNull()) | ||
} | ||
|
||
@Test | ||
fun `getTotpDataOrNull with incorrect host returns null`() { | ||
val uri = mockk<Uri> { | ||
every { scheme } returns "otpauth" | ||
every { host } returns "hotp" | ||
} | ||
|
||
assertNull(uri.getTotpDataOrNull()) | ||
} | ||
|
||
@Test | ||
fun `getTotpDataOrNull without secret returns null`() { | ||
val uri = mockk<Uri> { | ||
every { scheme } returns "otpauth" | ||
every { host } returns "totp" | ||
every { getQueryParameter("secret") } returns null | ||
} | ||
|
||
assertNull(uri.getTotpDataOrNull()) | ||
} | ||
|
||
@Test | ||
fun `getTotpDataOrNull with minimum required values returns TotpData with defaults`() { | ||
val secret = "secret" | ||
val uri = mockk<Uri> { | ||
every { scheme } returns "otpauth" | ||
every { host } returns "totp" | ||
every { pathSegments } returns emptyList() | ||
every { getQueryParameter("secret") } returns secret | ||
every { getQueryParameter("digits") } returns null | ||
every { getQueryParameter("issuer") } returns null | ||
every { getQueryParameter("period") } returns null | ||
every { getQueryParameter("algorithm") } returns null | ||
} | ||
|
||
val expectedResult = TotpData( | ||
uri = uri.toString(), | ||
issuer = null, | ||
accountName = null, | ||
secret = secret, | ||
digits = 6, | ||
period = 30, | ||
algorithm = CryptoHashAlgorithm.SHA_1, | ||
) | ||
|
||
assertEquals(expectedResult, uri.getTotpDataOrNull()) | ||
} | ||
|
||
@Test | ||
fun `getTotpDataOrNull with complete values returns custom TotpData`() { | ||
val secret = "secret" | ||
val digits = 8 | ||
val issuer = "Bitwarden" | ||
val period = 25 | ||
val algorithm = "sha256" | ||
val accountName = "[email protected]" | ||
val uri = mockk<Uri> { | ||
every { scheme } returns "otpauth" | ||
every { host } returns "totp" | ||
every { pathSegments } returns listOf("$issuer:$accountName") | ||
every { getQueryParameter("secret") } returns secret | ||
every { getQueryParameter("digits") } returns digits.toString() | ||
every { getQueryParameter("issuer") } returns issuer | ||
every { getQueryParameter("period") } returns period.toString() | ||
every { getQueryParameter("algorithm") } returns algorithm | ||
} | ||
val expectedResult = TotpData( | ||
uri = uri.toString(), | ||
issuer = issuer, | ||
accountName = accountName, | ||
secret = secret, | ||
digits = digits, | ||
period = period, | ||
algorithm = CryptoHashAlgorithm.SHA_256, | ||
) | ||
|
||
assertEquals(expectedResult, uri.getTotpDataOrNull()) | ||
} | ||
} |