diff options
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/data/password')
| -rw-r--r-- | app/src/main/java/dev/msfjarvis/aps/data/password/FieldItem.kt | 27 | ||||
| -rw-r--r-- | app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt | 96 |
2 files changed, 103 insertions, 20 deletions
diff --git a/app/src/main/java/dev/msfjarvis/aps/data/password/FieldItem.kt b/app/src/main/java/dev/msfjarvis/aps/data/password/FieldItem.kt new file mode 100644 index 00000000..b77ab5e3 --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/data/password/FieldItem.kt @@ -0,0 +1,27 @@ +package dev.msfjarvis.aps.data.password + +class FieldItem(val key: String, val value: String, val action: ActionType) { + enum class ActionType { + COPY, HIDE + } + + enum class ItemType(val type: String) { + USERNAME("Username"), PASSWORD("Password"), OTP("OTP") + } + + companion object { + + // Extra helper methods + fun createOtpField(otp: String): FieldItem { + return FieldItem(ItemType.OTP.type, otp, ActionType.COPY) + } + + fun createPasswordField(password: String): FieldItem { + return FieldItem(ItemType.PASSWORD.type, password, ActionType.HIDE) + } + + fun createUsernameField(username: String): FieldItem { + return FieldItem(ItemType.USERNAME.type, username, ActionType.COPY) + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt b/app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt index 3a6d9e2c..576a051b 100644 --- a/app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt +++ b/app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt @@ -4,12 +4,12 @@ */ package dev.msfjarvis.aps.data.password +import androidx.annotation.VisibleForTesting import com.github.michaelbull.result.get import dev.msfjarvis.aps.util.totp.Otp import dev.msfjarvis.aps.util.totp.TotpFinder import dev.msfjarvis.aps.util.totp.UriTotpFinder import java.io.ByteArrayOutputStream -import java.io.UnsupportedEncodingException import java.util.Date /** @@ -20,20 +20,28 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot val password: String val username: String? + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val digits: String + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val totpSecret: String? val totpPeriod: Long + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val totpAlgorithm: String - var extraContent: String - private set + val extraContent: String + val extraContentWithoutAuthData: String + val extraContentMap: Map<String, String> - @Throws(UnsupportedEncodingException::class) - constructor(os: ByteArrayOutputStream) : this(os.toString("UTF-8"), UriTotpFinder()) + constructor(os: ByteArrayOutputStream) : this(os.toString(Charsets.UTF_8.name()), UriTotpFinder()) init { val (foundPassword, passContent) = findAndStripPassword(content.split("\n".toRegex())) password = foundPassword extraContent = passContent.joinToString("\n") + extraContentWithoutAuthData = generateExtraContentWithoutAuthData() + extraContentMap = generateExtraContentPairs() username = findUsername() digits = findOtpDigits(content) totpSecret = findTotpSecret(content) @@ -45,6 +53,10 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot return extraContent.isNotEmpty() } + fun hasExtraContentWithoutAuthData(): Boolean { + return extraContentWithoutAuthData.isNotEmpty() + } + fun hasTotp(): Boolean { return totpSecret != null } @@ -59,23 +71,63 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot return Otp.calculateCode(totpSecret, Date().time / (1000 * totpPeriod), totpAlgorithm, digits).get() } - val extraContentWithoutAuthData by lazy(LazyThreadSafetyMode.NONE) { + private fun generateExtraContentWithoutAuthData(): String { var foundUsername = false - extraContent.splitToSequence("\n").filter { line -> - return@filter when { - USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } && !foundUsername -> { - foundUsername = true - false - } - line.startsWith("otpauth://", ignoreCase = true) || - line.startsWith("totp:", ignoreCase = true) -> { - false - } - else -> { - true + return extraContent + .lineSequence() + .filter { line -> + return@filter when { + USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } && !foundUsername -> { + foundUsername = true + false + } + line.startsWith("otpauth://", ignoreCase = true) || + line.startsWith("totp:", ignoreCase = true) -> { + false + } + else -> { + true + } } + }.joinToString(separator = "\n") + } + + private fun generateExtraContentPairs(): Map<String, String> { + fun MutableMap<String, String>.putOrAppend(key: String, value: String) { + if (value.isEmpty()) return + val existing = this[key] + this[key] = if (existing == null) { + value + } else { + "$existing\n$value" + } + } + + val items = mutableMapOf<String, String>() + // Take extraContentWithoutAuthData and onEach line perform the following tasks + extraContentWithoutAuthData.lines().forEach { line -> + // Split the line on ':' and save all the parts into an array + // "ABC : DEF:GHI" --> ["ABC", "DEF", "GHI"] + val splitArray = line.split(":") + // Take the first element of the array. This will be the key for the key-value pair. + // ["ABC ", " DEF", "GHI"] -> key = "ABC" + val key = splitArray.first().trimEnd() + // Remove the first element from the array and join the rest of the string again with ':' as separator. + // ["ABC ", " DEF", "GHI"] -> value = "DEF:GHI" + val value = splitArray.drop(1).joinToString(":").trimStart() + + if (key.isNotEmpty() && value.isNotEmpty()) { + // If both key and value are not empty, we can form a pair with this so add it to the map. + // key = "ABC", value = "DEF:GHI" + items[key] = value + } else { + // If either key or value is empty, we were not able to form proper key-value pair. + // So append the original line into an "EXTRA CONTENT" map entry + items.putOrAppend(EXTRA_CONTENT, line) } - }.joinToString(separator = "\n") + } + + return items } private fun findUsername(): String? { @@ -118,6 +170,9 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot companion object { + private const val EXTRA_CONTENT = "Extra Content" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val USERNAME_FIELDS = arrayOf( "login:", "username:", @@ -127,9 +182,10 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot "name:", "handle:", "id:", - "identity:" + "identity:", ) + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val PASSWORD_FIELDS = arrayOf( "password:", "secret:", |
