mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-08-30 22:05:19 +00:00
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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 =
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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:")
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user