aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/com
diff options
context:
space:
mode:
authorHarsh Shandilya <msfjarvis@gmail.com>2020-06-29 12:08:59 +0530
committerGitHub <noreply@github.com>2020-06-29 12:08:59 +0530
commit063c1a1144bb50845ecfb7d56eea16e4db4540e4 (patch)
treeeb237a470953264a54f2854f0e534bb2ab682927 /app/src/main/java/com
parent56c301dc7c5353d7f7021e04441104cfe42c063f (diff)
Reintroduce TOTP support (#890)
Co-authored-by: Fabian Henneke <fabian@henneke.me>
Diffstat (limited to 'app/src/main/java/com')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordEntry.kt78
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt10
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt49
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt6
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/model/PasswordEntry.kt113
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/Otp.kt88
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/TotpFinder.kt32
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/UriTotpFinder.kt57
11 files changed, 345 insertions, 94 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.kt
deleted file mode 100644
index d9168d39..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package com.zeapo.pwdstore
-
-import java.io.ByteArrayOutputStream
-import java.io.UnsupportedEncodingException
-
-/**
- * A single entry in password store.
- */
-class PasswordEntry(content: String) {
-
- val password: String
- val username: String?
- var extraContent: String
- private set
-
- @Throws(UnsupportedEncodingException::class)
- constructor(os: ByteArrayOutputStream) : this(os.toString("UTF-8"))
-
- init {
- val passContent = content.split("\n".toRegex(), 2).toTypedArray()
- password = passContent[0]
- extraContent = findExtraContent(passContent)
- username = findUsername()
- }
-
- fun hasExtraContent(): Boolean {
- return extraContent.isNotEmpty()
- }
-
- fun hasUsername(): Boolean {
- return username != null
- }
-
- val extraContentWithoutUsername by lazy {
- var usernameFound = false
- extraContent.splitToSequence("\n").filter { line ->
- if (usernameFound)
- return@filter true
- if (USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) }) {
- usernameFound = true
- return@filter false
- }
- true
- }.joinToString(separator = "\n")
- }
-
- private fun findUsername(): String? {
- extraContent.splitToSequence("\n").forEach { line ->
- for (prefix in USERNAME_FIELDS) {
- if (line.startsWith(prefix, ignoreCase = true))
- return line.substring(prefix.length).trimStart()
- }
- }
- return null
- }
-
- private fun findExtraContent(passContent: Array<String>): String {
- return if (passContent.size > 1) passContent[1] else ""
- }
-
- companion object {
- val USERNAME_FIELDS = arrayOf(
- "login:",
- "username:",
- "user:",
- "account:",
- "email:",
- "name:",
- "handle:",
- "id:",
- "identity:"
- )
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
index 26c86fc8..a58bde69 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
@@ -28,8 +28,8 @@ import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.i
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.splitLines
import kotlinx.coroutines.CoroutineScope
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt
index 101f96a0..d417484b 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt
@@ -19,8 +19,8 @@ import androidx.annotation.RequiresApi
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.e
-import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.utils.PasswordRepository
import java.io.File
import java.security.MessageDigest
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt
index 4c806dff..b66d068b 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt
@@ -16,12 +16,12 @@ import android.widget.Toast
import androidx.annotation.RequiresApi
import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e
-import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.autofill.oreo.AutofillAction
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
import com.zeapo.pwdstore.autofill.oreo.Credentials
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
import com.zeapo.pwdstore.autofill.oreo.FillableForm
+import com.zeapo.pwdstore.model.PasswordEntry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
index 5206a15f..fcadf119 100644
--- a/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
@@ -17,6 +17,7 @@ import android.view.WindowManager
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.CallSuper
+import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.Timber.tag
@@ -163,6 +164,7 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
return DateUtils.getRelativeTimeSpanString(this, timeStamp, true)
}
+
/**
* Base handling of OpenKeychain errors based on the error contained in [result]. Subclasses
* can use this when they want to default to sane error handling.
@@ -190,12 +192,16 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
* Copies provided [text] to the clipboard. Shows a [Snackbar] which can be disabled by passing
* [showSnackbar] as false.
*/
- fun copyTextToClipboard(text: String?, showSnackbar: Boolean = true) {
+ fun copyTextToClipboard(
+ text: String?,
+ showSnackbar: Boolean = true,
+ @StringRes snackbarTextRes: Int = R.string.clipboard_copied_text
+ ) {
val clipboard = clipboard ?: return
val clip = ClipData.newPlainText("pgp_handler_result_pm", text)
clipboard.setPrimaryClip(clip)
if (showSnackbar) {
- snackbar(message = resources.getString(R.string.clipboard_copied_text))
+ snackbar(message = resources.getString(snackbarTextRes))
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt
index b7d7adcd..ad0b16f7 100644
--- a/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt
@@ -17,17 +17,23 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e
-import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.databinding.DecryptLayoutBinding
+import com.zeapo.pwdstore.model.PasswordEntry
+import com.zeapo.pwdstore.utils.Otp
import com.zeapo.pwdstore.utils.viewBinding
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import me.msfjarvis.openpgpktx.util.OpenPgpApi
import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
import org.openintents.openpgp.IOpenPgpService2
import java.io.ByteArrayOutputStream
import java.io.File
+import java.util.Date
+import kotlin.time.ExperimentalTime
+import kotlin.time.seconds
class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
private val binding by viewBinding(DecryptLayoutBinding::inflate)
@@ -125,6 +131,7 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to)))
}
+ @OptIn(ExperimentalTime::class)
private fun decryptAndVerify(receivedIntent: Intent? = null) {
if (api == null) {
bindToOpenKeychain(this, openKeychainResult)
@@ -163,14 +170,16 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
}
if (entry.hasExtraContent()) {
- extraContentContainer.visibility = View.VISIBLE
- extraContent.typeface = monoTypeface
- extraContent.setText(entry.extraContentWithoutUsername)
- if (!showExtraContent) {
- extraContent.transformationMethod = PasswordTransformationMethod.getInstance()
+ if (entry.extraContentWithoutAuthData.isNotEmpty()) {
+ extraContentContainer.visibility = View.VISIBLE
+ extraContent.typeface = monoTypeface
+ extraContent.setText(entry.extraContentWithoutAuthData)
+ if (!showExtraContent) {
+ extraContent.transformationMethod = PasswordTransformationMethod.getInstance()
+ }
+ extraContentContainer.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutAuthData) }
+ extraContent.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutAuthData) }
}
- extraContentContainer.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) }
- extraContent.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) }
if (entry.hasUsername()) {
usernameText.typeface = monoTypeface
@@ -180,6 +189,30 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
} else {
usernameTextContainer.visibility = View.GONE
}
+
+ if (entry.hasTotp()) {
+ otpTextContainer.visibility = View.VISIBLE
+ otpTextContainer.setEndIconOnClickListener {
+ copyTextToClipboard(
+ otpText.text.toString(),
+ snackbarTextRes = R.string.clipboard_otp_copied_text
+ )
+ }
+ launch(Dispatchers.IO) {
+ repeat(Int.MAX_VALUE) {
+ val code = Otp.calculateCode(
+ entry.totpSecret!!,
+ Date().time / (1000 * entry.totpPeriod),
+ entry.totpAlgorithm,
+ entry.digits
+ ) ?: "Error"
+ withContext(Dispatchers.Main) {
+ otpText.setText(code)
+ }
+ delay(30.seconds)
+ }
+ }
+ }
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
index 67bf9926..81a73988 100644
--- a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
@@ -17,16 +17,16 @@ import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.zeapo.pwdstore.PasswordEntry
-import com.zeapo.pwdstore.utils.isInsideRepository
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
import com.zeapo.pwdstore.databinding.PasswordCreationActivityBinding
+import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.ui.dialogs.PasswordGeneratorDialogFragment
import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.commitChange
+import com.zeapo.pwdstore.utils.isInsideRepository
import com.zeapo.pwdstore.utils.snackbar
import com.zeapo.pwdstore.utils.viewBinding
import kotlinx.coroutines.Dispatchers
@@ -108,7 +108,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
// input lag.
if (username != null) {
filename.setText(username)
- extraContent.setText(entry.extraContentWithoutUsername)
+ extraContent.setText(entry.extraContentWithoutAuthData)
}
}
updateEncryptUsernameState()
diff --git a/app/src/main/java/com/zeapo/pwdstore/model/PasswordEntry.kt b/app/src/main/java/com/zeapo/pwdstore/model/PasswordEntry.kt
new file mode 100644
index 00000000..da2d57c6
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/model/PasswordEntry.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.model
+
+import com.zeapo.pwdstore.utils.TotpFinder
+import com.zeapo.pwdstore.utils.UriTotpFinder
+import java.io.ByteArrayOutputStream
+import java.io.UnsupportedEncodingException
+
+/**
+ * A single entry in password store. [totpFinder] is an implementation of [TotpFinder] that let's us
+ * abstract out the Android-specific part and continue testing the class in the JVM.
+ */
+class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTotpFinder()) {
+
+ val password: String
+ val username: String?
+ val digits: String
+ val totpSecret: String?
+ val totpPeriod: Long
+ val totpAlgorithm: String
+ var extraContent: String
+ private set
+
+ @Throws(UnsupportedEncodingException::class)
+ constructor(os: ByteArrayOutputStream) : this(os.toString("UTF-8"), UriTotpFinder())
+
+ init {
+ val passContent = content.split("\n".toRegex(), 2).toTypedArray()
+ password = passContent[0]
+ extraContent = findExtraContent(passContent)
+ username = findUsername()
+ digits = findOtpDigits(content)
+ totpSecret = findTotpSecret(content)
+ totpPeriod = findTotpPeriod(content)
+ totpAlgorithm = findTotpAlgorithm(content)
+ }
+
+ fun hasExtraContent(): Boolean {
+ return extraContent.isNotEmpty()
+ }
+
+ fun hasTotp(): Boolean {
+ return totpSecret != null
+ }
+
+ fun hasUsername(): Boolean {
+ return username != null
+ }
+
+ val extraContentWithoutAuthData by lazy {
+ extraContent.splitToSequence("\n").filter { line ->
+ return@filter when {
+ USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } -> {
+ false
+ }
+ line.startsWith("otpauth://", ignoreCase = true) ||
+ line.startsWith("totp:", ignoreCase = true) -> {
+ false
+ }
+ else -> {
+ true
+ }
+ }
+ }.joinToString(separator = "\n")
+ }
+
+ private fun findUsername(): String? {
+ extraContent.splitToSequence("\n").forEach { line ->
+ for (prefix in USERNAME_FIELDS) {
+ if (line.startsWith(prefix, ignoreCase = true))
+ return line.substring(prefix.length).trimStart()
+ }
+ }
+ return null
+ }
+
+ private fun findExtraContent(passContent: Array<String>): String {
+ return if (passContent.size > 1) passContent[1] else ""
+ }
+
+ private fun findTotpSecret(decryptedContent: String): String? {
+ return totpFinder.findSecret(decryptedContent)
+ }
+
+ private fun findOtpDigits(decryptedContent: String): String {
+ return totpFinder.findDigits(decryptedContent)
+ }
+
+ private fun findTotpPeriod(decryptedContent: String): Long {
+ return totpFinder.findPeriod(decryptedContent)
+ }
+
+ private fun findTotpAlgorithm(decryptedContent: String): String {
+ return totpFinder.findAlgorithm(decryptedContent)
+ }
+
+ companion object {
+ val USERNAME_FIELDS = arrayOf(
+ "login:",
+ "username:",
+ "user:",
+ "account:",
+ "email:",
+ "name:",
+ "handle:",
+ "id:",
+ "identity:"
+ )
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Otp.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Otp.kt
new file mode 100644
index 00000000..b95b9902
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/Otp.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package com.zeapo.pwdstore.utils
+
+import com.github.ajalt.timberkt.e
+import org.apache.commons.codec.binary.Base32
+import java.nio.ByteBuffer
+import java.security.InvalidKeyException
+import java.security.NoSuchAlgorithmException
+import java.util.Locale
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+import kotlin.experimental.and
+
+object Otp {
+ private val BASE_32 = Base32()
+ private val STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY".toCharArray()
+ init {
+ check(STEAM_ALPHABET.size == 26)
+ }
+
+ fun calculateCode(secret: String, counter: Long, algorithm: String, digits: String): String? {
+ val algo = "Hmac${algorithm.toUpperCase(Locale.ROOT)}"
+ val decodedSecret = try {
+ BASE_32.decode(secret)
+ } catch (e: Exception) {
+ e(e) { "Failed to decode secret" }
+ return null
+ }
+ val secretKey = SecretKeySpec(decodedSecret, algo)
+ val digest = try {
+ Mac.getInstance(algo).run {
+ init(secretKey)
+ doFinal(ByteBuffer.allocate(8).putLong(counter).array())
+ }
+ } catch (e: NoSuchAlgorithmException) {
+ e(e)
+ return null
+ } catch (e: InvalidKeyException) {
+ e(e) { "Key is malformed" }
+ return null
+ }
+ // Least significant 4 bits are used as an offset into the digest.
+ val offset = (digest.last() and 0xf).toInt()
+ // Extract 32 bits at the offset and clear the most significant bit.
+ val code = digest.copyOfRange(offset, offset + 4)
+ code[0] = (0x7f and code[0].toInt()).toByte()
+ val codeInt = ByteBuffer.wrap(code).int
+ check(codeInt > 0)
+ return if (digits == "s") {
+ // Steam
+ var remainingCodeInt = codeInt
+ buildString {
+ repeat(5) {
+ append(STEAM_ALPHABET[remainingCodeInt % 26])
+ remainingCodeInt /= 26
+ }
+ }
+ } else {
+ // Base 10, 6 to 10 digits
+ val numDigits = digits.toIntOrNull()
+ when {
+ numDigits == null -> {
+ e { "Digits specifier has to be either 's' or numeric" }
+ return null
+ }
+ numDigits < 6 -> {
+ e { "TOTP codes have to be at least 6 digits long" }
+ return null
+ }
+ numDigits > 10 -> {
+ e { "TOTP codes can be at most 10 digits long" }
+ return null
+ }
+ else -> {
+ // 2^31 = 2_147_483_648, so we can extract at most 10 digits with the first one
+ // always being 0, 1, or 2. Pad with leading zeroes.
+ val codeStringBase10 = codeInt.toString(10).padStart(10, '0')
+ check(codeStringBase10.length == 10)
+ codeStringBase10.takeLast(numDigits)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/TotpFinder.kt b/app/src/main/java/com/zeapo/pwdstore/utils/TotpFinder.kt
new file mode 100644
index 00000000..13a47543
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/TotpFinder.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package com.zeapo.pwdstore.utils
+
+/**
+ * Defines a class that can extract relevant parts of a TOTP URL for use by the app.
+ */
+interface TotpFinder {
+
+ /**
+ * Get the TOTP secret from the given extra content.
+ */
+ fun findSecret(content: String): String?
+
+ /**
+ * Get the number of digits required in the final OTP.
+ */
+ fun findDigits(content: String): String
+
+ /**
+ * Get the TOTP timeout period.
+ */
+ fun findPeriod(content: String): Long
+
+ /**
+ * Get the algorithm for the TOTP secret.
+ */
+ fun findAlgorithm(content: String): String
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/UriTotpFinder.kt b/app/src/main/java/com/zeapo/pwdstore/utils/UriTotpFinder.kt
new file mode 100644
index 00000000..faa349d1
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/UriTotpFinder.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package com.zeapo.pwdstore.utils
+
+import android.net.Uri
+
+/**
+ * [Uri] backed TOTP URL parser.
+ */
+class UriTotpFinder : TotpFinder {
+ override fun findSecret(content: String): String? {
+ content.split("\n".toRegex()).forEach { line ->
+ if (line.startsWith("otpauth://totp/")) {
+ return Uri.parse(line).getQueryParameter("secret")
+ }
+ if (line.startsWith("totp:", ignoreCase = true)) {
+ return line.split(": *".toRegex(), 2).toTypedArray()[1]
+ }
+ }
+ return null
+ }
+
+ override fun findDigits(content: String): String {
+ content.split("\n".toRegex()).forEach { line ->
+ if (line.startsWith("otpauth://totp/") &&
+ Uri.parse(line).getQueryParameter("digits") != null) {
+ return Uri.parse(line).getQueryParameter("digits")!!
+ }
+ }
+ return "6"
+ }
+
+ override fun findPeriod(content: String): Long {
+ content.split("\n".toRegex()).forEach { line ->
+ if (line.startsWith("otpauth://totp/") &&
+ Uri.parse(line).getQueryParameter("period") != null) {
+ val period = Uri.parse(line).getQueryParameter("period")!!.toLongOrNull()
+ if (period != null && period > 0)
+ return period
+ }
+ }
+ return 30
+ }
+
+ override fun findAlgorithm(content: String): String {
+ content.split("\n".toRegex()).forEach { line ->
+ if (line.startsWith("otpauth://totp/") &&
+ Uri.parse(line).getQueryParameter("algorithm") != null) {
+ return Uri.parse(line).getQueryParameter("algorithm")!!
+ }
+ }
+ return "sha1"
+ }
+}