Refactor TOTP implementation and expand SteamGuard hacks (#1460)

* UriTotpFinder: commonize query parameter handling

* gitignore: add more IDEA files

* TotpFinder: add `findIssuer`

* PasswordEntry: don't eagerly fetch TOTP related fields

* format-common: expand SteamGuard workaround

* CHANGELOG: add SteamGuard workaround
This commit is contained in:
Harsh Shandilya
2021-07-17 03:13:16 +05:30
committed by GitHub
parent fd6d0e52fc
commit 921e9f96b9
8 changed files with 50 additions and 31 deletions

1
.gitignore vendored
View File

@@ -104,6 +104,7 @@ obj/
.idea/assetWizardSettings.xml .idea/assetWizardSettings.xml
.idea/gradle.xml .idea/gradle.xml
.idea/jarRepositories.xml .idea/jarRepositories.xml
.idea/runConfigurations.xml
# OS-specific files # OS-specific files
.DS_Store .DS_Store

View File

@@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- Parse extra content as individual fields - Parse extra content as individual fields
- Improve search result filtering logic - Improve search result filtering logic
- Allow pinning shortcuts directly to the launcher home screen - Allow pinning shortcuts directly to the launcher home screen
- Another workaround for SteamGuard's non-standard OTP format
### Fixed ### Fixed

View File

@@ -24,32 +24,29 @@ class UriTotpFinder @Inject constructor() : TotpFinder {
} }
override fun findDigits(content: String): String { override fun findDigits(content: String): String {
content.split("\n".toRegex()).forEach { line -> return getQueryParameter(content, "digits") ?: "6"
if (line.startsWith(TOTP_FIELDS[0]) && Uri.parse(line).getQueryParameter("digits") != null) {
return Uri.parse(line).getQueryParameter("digits")!!
}
}
return "6"
} }
override fun findPeriod(content: String): Long { override fun findPeriod(content: String): Long {
content.split("\n".toRegex()).forEach { line -> return getQueryParameter(content, "period")?.toLongOrNull() ?: 30
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
} }
override fun findAlgorithm(content: String): String { override fun findAlgorithm(content: String): String {
return getQueryParameter(content, "algorithm") ?: "sha1"
}
override fun findIssuer(content: String): String? {
return getQueryParameter(content, "issuer") ?: Uri.parse(content).authority
}
private fun getQueryParameter(content: String, parameterName: String): String? {
content.split("\n".toRegex()).forEach { line -> content.split("\n".toRegex()).forEach { line ->
if (line.startsWith(TOTP_FIELDS[0]) && Uri.parse(line).getQueryParameter("algorithm") != null val uri = Uri.parse(line)
) { if (line.startsWith(TOTP_FIELDS[0]) && uri.getQueryParameter(parameterName) != null) {
return Uri.parse(line).getQueryParameter("algorithm")!! return uri.getQueryParameter(parameterName)
} }
} }
return "sha1" return null
} }
companion object { companion object {

View File

@@ -45,6 +45,12 @@ class UriTotpFinderTest {
assertEquals("SHA256", totpFinder.findAlgorithm(PASS_FILE_CONTENT)) assertEquals("SHA256", totpFinder.findAlgorithm(PASS_FILE_CONTENT))
} }
@Test
fun findIssuer() {
assertEquals("ACME Co", totpFinder.findIssuer(TOTP_URI))
assertEquals("ACME Co", totpFinder.findIssuer(PASS_FILE_CONTENT))
}
companion object { companion object {
const val TOTP_URI = const val TOTP_URI =

View File

@@ -21,6 +21,7 @@ public abstract interface class dev/msfjarvis/aps/util/totp/TotpFinder {
public static final field Companion Ldev/msfjarvis/aps/util/totp/TotpFinder$Companion; public static final field Companion Ldev/msfjarvis/aps/util/totp/TotpFinder$Companion;
public abstract fun findAlgorithm (Ljava/lang/String;)Ljava/lang/String; public abstract fun findAlgorithm (Ljava/lang/String;)Ljava/lang/String;
public abstract fun findDigits (Ljava/lang/String;)Ljava/lang/String; public abstract fun findDigits (Ljava/lang/String;)Ljava/lang/String;
public abstract fun findIssuer (Ljava/lang/String;)Ljava/lang/String;
public abstract fun findPeriod (Ljava/lang/String;)J public abstract fun findPeriod (Ljava/lang/String;)J
public abstract fun findSecret (Ljava/lang/String;)Ljava/lang/String; public abstract fun findSecret (Ljava/lang/String;)Ljava/lang/String;
} }

View File

@@ -68,10 +68,7 @@ constructor(
* and usernames stripped. * and usernames stripped.
*/ */
public val extraContentWithoutAuthData: String public val extraContentWithoutAuthData: String
private val digits: String
private val totpSecret: String? private val totpSecret: String?
private val totpPeriod: Long
private val totpAlgorithm: String
init { init {
val (foundPassword, passContent) = findAndStripPassword(content.split("\n".toRegex())) val (foundPassword, passContent) = findAndStripPassword(content.split("\n".toRegex()))
@@ -80,17 +77,18 @@ constructor(
extraContentWithoutAuthData = generateExtraContentWithoutAuthData() extraContentWithoutAuthData = generateExtraContentWithoutAuthData()
extraContent = generateExtraContentPairs() extraContent = generateExtraContentPairs()
username = findUsername() username = findUsername()
digits = totpFinder.findDigits(content)
totpSecret = totpFinder.findSecret(content) totpSecret = totpFinder.findSecret(content)
totpPeriod = totpFinder.findPeriod(content)
totpAlgorithm = totpFinder.findAlgorithm(content)
if (totpSecret != null) { if (totpSecret != null) {
scope.launch { scope.launch {
updateTotp(clock.millis()) val digits = totpFinder.findDigits(content)
val totpPeriod = totpFinder.findPeriod(content)
val totpAlgorithm = totpFinder.findAlgorithm(content)
val issuer = totpFinder.findIssuer(content)
val remainingTime = totpPeriod - (clock.millis() % totpPeriod) val remainingTime = totpPeriod - (clock.millis() % totpPeriod)
updateTotp(clock.millis(), totpPeriod, totpAlgorithm, digits, issuer)
delay(Duration.seconds(remainingTime)) delay(Duration.seconds(remainingTime))
repeat(Int.MAX_VALUE) { repeat(Int.MAX_VALUE) {
updateTotp(clock.millis()) updateTotp(clock.millis(), totpPeriod, totpAlgorithm, digits, issuer)
delay(Duration.seconds(totpPeriod)) delay(Duration.seconds(totpPeriod))
} }
} }
@@ -186,9 +184,15 @@ constructor(
return null return null
} }
private fun updateTotp(millis: Long) { private fun updateTotp(
millis: Long,
totpPeriod: Long,
totpAlgorithm: String,
digits: String,
issuer: String?,
) {
if (totpSecret != null) { if (totpSecret != null) {
Otp.calculateCode(totpSecret, millis / (1000 * totpPeriod), totpAlgorithm, digits) Otp.calculateCode(totpSecret, millis / (1000 * totpPeriod), totpAlgorithm, digits, issuer)
.mapBoth({ code -> _totp.value = code }, { throwable -> throw throwable }) .mapBoth({ code -> _totp.value = code }, { throwable -> throw throwable })
} }
} }

View File

@@ -23,8 +23,13 @@ internal object Otp {
check(STEAM_ALPHABET.size == 26) check(STEAM_ALPHABET.size == 26)
} }
fun calculateCode(secret: String, counter: Long, algorithm: String, digits: String) = fun calculateCode(
runCatching { secret: String,
counter: Long,
algorithm: String,
digits: String,
issuer: String?,
) = runCatching {
val algo = "Hmac${algorithm.uppercase(Locale.ROOT)}" val algo = "Hmac${algorithm.uppercase(Locale.ROOT)}"
val decodedSecret = BASE_32.decode(secret) val decodedSecret = BASE_32.decode(secret)
val secretKey = SecretKeySpec(decodedSecret, algo) val secretKey = SecretKeySpec(decodedSecret, algo)
@@ -40,8 +45,9 @@ internal object Otp {
code[0] = (0x7f and code[0].toInt()).toByte() code[0] = (0x7f and code[0].toInt()).toByte()
val codeInt = ByteBuffer.wrap(code).int val codeInt = ByteBuffer.wrap(code).int
check(codeInt > 0) check(codeInt > 0)
if (digits == "s") { // SteamGuard is a horrible OTP implementation that generates non-standard 5 digit OTPs as well
// Steam // as uses a custom character set.
if (digits == "s" || issuer == "Steam") {
var remainingCodeInt = codeInt var remainingCodeInt = codeInt
buildString { buildString {
repeat(5) { repeat(5) {

View File

@@ -20,6 +20,9 @@ public interface TotpFinder {
/** Get the algorithm for the TOTP secret. */ /** Get the algorithm for the TOTP secret. */
public fun findAlgorithm(content: String): String public fun findAlgorithm(content: String): String
/** Get the issuer for the TOTP secret, if any. */
public fun findIssuer(content: String): String?
public companion object { public companion object {
public val TOTP_FIELDS: Array<String> = arrayOf("otpauth://totp", "totp:") public val TOTP_FIELDS: Array<String> = arrayOf("otpauth://totp", "totp:")
} }