diff options
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/util/totp')
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:") + } } |
