summaryrefslogtreecommitdiffstats
path: root/app/src/main/java/dev/msfjarvis/aps/data
diff options
context:
space:
mode:
authorAditya Wasan <adityawasan55@gmail.com>2021-02-19 13:57:57 +0530
committerGitHub <noreply@github.com>2021-02-19 08:27:57 +0000
commitd17ff0d9251bd2ad84778535dabe4884a4cf9d76 (patch)
tree57c2cfd031632e8687c038bd35ff2b39f61f6d77 /app/src/main/java/dev/msfjarvis/aps/data
parent92ece7dbb5607258bcf954963009bf1f9411ab07 (diff)
downloadHardenedPass-d17ff0d9251bd2ad84778535dabe4884a4cf9d76.tar.gz
HardenedPass-d17ff0d9251bd2ad84778535dabe4884a4cf9d76.zip
Parse extra content into key value pairs (#1321)
* ui: add skeleton recyclerview to parse extra content Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * ui: add recyclerview and update PasswordEntry to create map of key-value pairs Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * password-entry: When key-value pair is not correctly formed, display it as Extra Content Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * Fix formatting Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * bug: update otp code on main thread Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * Add complete string if key-value pair cannot be formed Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * test: add a few tests for key-value parsing logic Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * prefs: remove SHOW_EXTRA_CONTENT from shared preferences Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * Update CHANGELOG.md * Cleanup and refactor Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * PasswordEntryTest: silence nullability warning Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * PasswordEntry: simplify constructor Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * PasswordEntry: annotate test-enablement visibility Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Reintroduce the catch-all field Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * update parsing logic Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * add one more test case Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * Add missing newlines Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Remove unnecessary scrollview Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * rv: do not return if hasExtraContent is false Signed-off-by: Aditya Wasan <adityawasan55@gmail.com> * Don't anchor RV to bottom Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/data')
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/data/password/FieldItem.kt27
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt96
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:",