aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/dev/msfjarvis/aps/util/totp
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/util/totp')
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/totp/Otp.kt99
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/totp/TotpFinder.kt28
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/totp/UriTotpFinder.kt87
3 files changed, 98 insertions, 116 deletions
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/totp/Otp.kt b/app/src/main/java/dev/msfjarvis/aps/util/totp/Otp.kt
index c0fbb8c4..1ef155a5 100644
--- a/app/src/main/java/dev/msfjarvis/aps/util/totp/Otp.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/util/totp/Otp.kt
@@ -16,58 +16,59 @@ import org.apache.commons.codec.binary.Base32
object Otp {
- private val BASE_32 = Base32()
- private val STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY".toCharArray()
+ private val BASE_32 = Base32()
+ private val STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY".toCharArray()
- init {
- check(STEAM_ALPHABET.size == 26)
- }
+ init {
+ check(STEAM_ALPHABET.size == 26)
+ }
- fun calculateCode(secret: String, counter: Long, algorithm: String, digits: String) = runCatching {
- val algo = "Hmac${algorithm.toUpperCase(Locale.ROOT)}"
- val decodedSecret = BASE_32.decode(secret)
- val secretKey = SecretKeySpec(decodedSecret, algo)
- val digest = Mac.getInstance(algo).run {
- init(secretKey)
- doFinal(ByteBuffer.allocate(8).putLong(counter).array())
+ fun calculateCode(secret: String, counter: Long, algorithm: String, digits: String) = runCatching {
+ val algo = "Hmac${algorithm.toUpperCase(Locale.ROOT)}"
+ val decodedSecret = BASE_32.decode(secret)
+ val secretKey = SecretKeySpec(decodedSecret, algo)
+ val digest =
+ Mac.getInstance(algo).run {
+ init(secretKey)
+ doFinal(ByteBuffer.allocate(8).putLong(counter).array())
+ }
+ // 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)
+ 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 -> {
+ return Err(IllegalArgumentException("Digits specifier has to be either 's' or numeric"))
+ }
+ numDigits < 6 -> {
+ return Err(IllegalArgumentException("TOTP codes have to be at least 6 digits long"))
+ }
+ numDigits > 10 -> {
+ return Err(IllegalArgumentException("TOTP codes can be at most 10 digits long"))
}
- // 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)
- 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 -> {
- return Err(IllegalArgumentException("Digits specifier has to be either 's' or numeric"))
- }
- numDigits < 6 -> {
- return Err(IllegalArgumentException("TOTP codes have to be at least 6 digits long"))
- }
- numDigits > 10 -> {
- return Err(IllegalArgumentException("TOTP codes can be at most 10 digits long"))
- }
- 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)
- }
- }
+ 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/dev/msfjarvis/aps/util/totp/TotpFinder.kt b/app/src/main/java/dev/msfjarvis/aps/util/totp/TotpFinder.kt
index 8144095d..e787fea5 100644
--- a/app/src/main/java/dev/msfjarvis/aps/util/totp/TotpFinder.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/util/totp/TotpFinder.kt
@@ -5,28 +5,18 @@
package dev.msfjarvis.aps.util.totp
-/**
- * Defines a class that can extract relevant parts of a TOTP URL for use by the app.
- */
+/** 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 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 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 TOTP timeout period. */
+ fun findPeriod(content: String): Long
- /**
- * Get the algorithm for the TOTP secret.
- */
- fun findAlgorithm(content: String): String
+ /** Get the algorithm for the TOTP secret. */
+ fun findAlgorithm(content: String): String
}
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/totp/UriTotpFinder.kt b/app/src/main/java/dev/msfjarvis/aps/util/totp/UriTotpFinder.kt
index c205b94b..fa3ff28a 100644
--- a/app/src/main/java/dev/msfjarvis/aps/util/totp/UriTotpFinder.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/util/totp/UriTotpFinder.kt
@@ -7,60 +7,51 @@ package dev.msfjarvis.aps.util.totp
import android.net.Uri
-/**
- * [Uri] backed TOTP URL parser.
- */
+/** [Uri] backed TOTP URL parser. */
class UriTotpFinder : TotpFinder {
- override fun findSecret(content: String): String? {
- content.split("\n".toRegex()).forEach { line ->
- if (line.startsWith(TOTP_FIELDS[0])) {
- return Uri.parse(line).getQueryParameter("secret")
- }
- if (line.startsWith(TOTP_FIELDS[1], ignoreCase = true)) {
- return line.split(": *".toRegex(), 2).toTypedArray()[1]
- }
- }
- return null
+ override fun findSecret(content: String): String? {
+ content.split("\n".toRegex()).forEach { line ->
+ if (line.startsWith(TOTP_FIELDS[0])) {
+ return Uri.parse(line).getQueryParameter("secret")
+ }
+ if (line.startsWith(TOTP_FIELDS[1], ignoreCase = true)) {
+ return line.split(": *".toRegex(), 2).toTypedArray()[1]
+ }
}
-
- override fun findDigits(content: String): String {
- content.split("\n".toRegex()).forEach { line ->
- if (line.startsWith(TOTP_FIELDS[0]) &&
- Uri.parse(line).getQueryParameter("digits") != null) {
- return Uri.parse(line).getQueryParameter("digits")!!
- }
- }
- return "6"
+ return null
+ }
+
+ override fun findDigits(content: String): String {
+ content.split("\n".toRegex()).forEach { line ->
+ if (line.startsWith(TOTP_FIELDS[0]) && Uri.parse(line).getQueryParameter("digits") != null) {
+ return Uri.parse(line).getQueryParameter("digits")!!
+ }
}
-
- override fun findPeriod(content: String): Long {
- content.split("\n".toRegex()).forEach { line ->
- if (line.startsWith(TOTP_FIELDS[0]) &&
- Uri.parse(line).getQueryParameter("period") != null) {
- val period = Uri.parse(line).getQueryParameter("period")!!.toLongOrNull()
- if (period != null && period > 0)
- return period
- }
- }
- return 30
+ return "6"
+ }
+
+ override fun findPeriod(content: String): Long {
+ content.split("\n".toRegex()).forEach { line ->
+ if (line.startsWith(TOTP_FIELDS[0]) && Uri.parse(line).getQueryParameter("period") != null) {
+ val period = Uri.parse(line).getQueryParameter("period")!!.toLongOrNull()
+ if (period != null && period > 0) return period
+ }
}
-
- override fun findAlgorithm(content: String): String {
- content.split("\n".toRegex()).forEach { line ->
- if (line.startsWith(TOTP_FIELDS[0]) &&
- Uri.parse(line).getQueryParameter("algorithm") != null) {
- return Uri.parse(line).getQueryParameter("algorithm")!!
- }
- }
- return "sha1"
+ return 30
+ }
+
+ override fun findAlgorithm(content: String): String {
+ content.split("\n".toRegex()).forEach { line ->
+ if (line.startsWith(TOTP_FIELDS[0]) && Uri.parse(line).getQueryParameter("algorithm") != null) {
+ return Uri.parse(line).getQueryParameter("algorithm")!!
+ }
}
+ return "sha1"
+ }
- companion object {
+ companion object {
- val TOTP_FIELDS = arrayOf(
- "otpauth://totp",
- "totp:"
- )
- }
+ val TOTP_FIELDS = arrayOf("otpauth://totp", "totp:")
+ }
}