aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/build.gradle.kts2
-rw-r--r--app/src/androidTest/java/dev/msfjarvis/aps/data/password/PasswordEntryAndroidTest.kt122
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt195
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt11
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt40
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt15
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/autofill/AutofillPreferences.kt4
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/totp/Otp.kt74
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/totp/TotpFinder.kt22
-rw-r--r--app/src/test/java/dev/msfjarvis/aps/data/password/PasswordEntryTest.kt195
-rw-r--r--app/src/test/java/dev/msfjarvis/aps/util/totp/OtpTest.kt73
11 files changed, 39 insertions, 714 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 41c561f4..8b2b18f9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -49,6 +49,7 @@ dependencies {
compileOnly(libs.androidx.annotation)
coreLibraryDesugaring(libs.android.desugarJdkLibs)
implementation(projects.autofillParser)
+ implementation(projects.formatCommon)
implementation(projects.openpgpKtx)
implementation(libs.androidx.activityKtx)
implementation(libs.androidx.appcompat)
@@ -74,7 +75,6 @@ dependencies {
implementation(libs.aps.zxingAndroidEmbedded)
implementation(libs.thirdparty.bouncycastle)
- implementation(libs.thirdparty.commons.codec)
implementation(libs.thirdparty.eddsa)
implementation(libs.thirdparty.fastscroll)
implementation(libs.thirdparty.jgit) {
diff --git a/app/src/androidTest/java/dev/msfjarvis/aps/data/password/PasswordEntryAndroidTest.kt b/app/src/androidTest/java/dev/msfjarvis/aps/data/password/PasswordEntryAndroidTest.kt
deleted file mode 100644
index 97f3539c..00000000
--- a/app/src/androidTest/java/dev/msfjarvis/aps/data/password/PasswordEntryAndroidTest.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package dev.msfjarvis.aps.data.password
-
-import com.github.michaelbull.result.get
-import dev.msfjarvis.aps.util.totp.Otp
-import dev.msfjarvis.aps.util.totp.UriTotpFinder
-import java.util.Date
-import kotlin.test.assertEquals
-import kotlin.test.assertFalse
-import kotlin.test.assertNotNull
-import kotlin.test.assertNull
-import kotlin.test.assertTrue
-import org.junit.Test
-
-class PasswordEntryAndroidTest {
-
- private fun makeEntry(content: String) = PasswordEntry(content, UriTotpFinder())
-
- @Test
- fun testGetPassword() {
- assertEquals("fooooo", makeEntry("fooooo\nbla\n").password)
- assertEquals("fooooo", makeEntry("fooooo\nbla").password)
- assertEquals("fooooo", makeEntry("fooooo\n").password)
- assertEquals("fooooo", makeEntry("fooooo").password)
- assertEquals("", makeEntry("\nblubb\n").password)
- assertEquals("", makeEntry("\nblubb").password)
- assertEquals("", makeEntry("\n").password)
- assertEquals("", makeEntry("").password)
- }
-
- @Test
- fun testGetExtraContent() {
- assertEquals("bla\n", makeEntry("fooooo\nbla\n").extraContent)
- assertEquals("bla", makeEntry("fooooo\nbla").extraContent)
- assertEquals("", makeEntry("fooooo\n").extraContent)
- assertEquals("", makeEntry("fooooo").extraContent)
- assertEquals("blubb\n", makeEntry("\nblubb\n").extraContent)
- assertEquals("blubb", makeEntry("\nblubb").extraContent)
- assertEquals("", makeEntry("\n").extraContent)
- assertEquals("", makeEntry("").extraContent)
- }
-
- @Test
- fun testGetUsername() {
- for (field in PasswordEntry.USERNAME_FIELDS) {
- assertEquals("username", makeEntry("\n$field username").username)
- assertEquals("username", makeEntry("\n${field.toUpperCase()} username").username)
- }
- assertEquals("username", makeEntry("secret\nextra\nlogin: username\ncontent\n").username)
- assertEquals("username", makeEntry("\nextra\nusername: username\ncontent\n").username)
- assertEquals("username", makeEntry("\nUSERNaMe: username\ncontent\n").username)
- assertEquals("username", makeEntry("\nlogin: username").username)
- assertEquals("foo@example.com", makeEntry("\nemail: foo@example.com").username)
- assertEquals("username", makeEntry("\nidentity: username\nlogin: another_username").username)
- assertEquals("username", makeEntry("\nLOGiN:username").username)
- assertNull(makeEntry("secret\nextra\ncontent\n").username)
- }
-
- @Test
- fun testHasUsername() {
- assertTrue(makeEntry("secret\nextra\nlogin: username\ncontent\n").hasUsername())
- assertFalse(makeEntry("secret\nextra\ncontent\n").hasUsername())
- assertFalse(makeEntry("secret\nlogin failed\n").hasUsername())
- assertFalse(makeEntry("\n").hasUsername())
- assertFalse(makeEntry("").hasUsername())
- }
-
- @Test
- fun testGeneratesOtpFromTotpUri() {
- val entry = makeEntry("secret\nextra\n$TOTP_URI")
- assertTrue(entry.hasTotp())
- val code =
- Otp.calculateCode(
- entry.totpSecret!!,
- // The hardcoded date value allows this test to stay reproducible.
- Date(8640000).time / (1000 * entry.totpPeriod),
- entry.totpAlgorithm,
- entry.digits
- )
- .get()
- assertNotNull(code) { "Generated OTP cannot be null" }
- assertEquals(entry.digits.toInt(), code.length)
- assertEquals("545293", code)
- }
-
- @Test
- fun testGeneratesOtpWithOnlyUriInFile() {
- val entry = makeEntry(TOTP_URI)
- assertTrue(entry.password.isEmpty())
- assertTrue(entry.hasTotp())
- val code =
- Otp.calculateCode(
- entry.totpSecret!!,
- // The hardcoded date value allows this test to stay reproducible.
- Date(8640000).time / (1000 * entry.totpPeriod),
- entry.totpAlgorithm,
- entry.digits
- )
- .get()
- assertNotNull(code) { "Generated OTP cannot be null" }
- assertEquals(entry.digits.toInt(), code.length)
- assertEquals("545293", code)
- }
-
- @Test
- fun testOnlyLooksForUriInFirstLine() {
- val entry = makeEntry("id:\n$TOTP_URI")
- assertTrue(entry.password.isNotEmpty())
- assertTrue(entry.hasTotp())
- assertFalse(entry.hasUsername())
- }
-
- companion object {
-
- const val TOTP_URI =
- "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"
- }
-}
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
deleted file mode 100644
index 8a2ca3c6..00000000
--- a/app/src/main/java/dev/msfjarvis/aps/data/password/PasswordEntry.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-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.util.Date
-
-/**
- * A single entry in password store. [totpFinder] is an implementation of [TotpFinder] that let's us
- * abstract out the Android-specific part and continue testing the class in the JVM.
- */
-class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTotpFinder()) {
-
- 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
- val extraContent: String
- val extraContentWithoutAuthData: String
- val extraContentMap: Map<String, String>
-
- 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)
- totpPeriod = findTotpPeriod(content)
- totpAlgorithm = findTotpAlgorithm(content)
- }
-
- fun hasExtraContent(): Boolean {
- return extraContent.isNotEmpty()
- }
-
- fun hasExtraContentWithoutAuthData(): Boolean {
- return extraContentWithoutAuthData.isNotEmpty()
- }
-
- fun hasTotp(): Boolean {
- return totpSecret != null
- }
-
- fun hasUsername(): Boolean {
- return username != null
- }
-
- fun calculateTotpCode(): String? {
- if (totpSecret == null) return null
- return Otp.calculateCode(totpSecret, Date().time / (1000 * totpPeriod), totpAlgorithm, digits).get()
- }
-
- private fun generateExtraContentWithoutAuthData(): String {
- var foundUsername = false
- 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)
- }
- }
-
- return items
- }
-
- private fun findUsername(): String? {
- extraContent.splitToSequence("\n").forEach { line ->
- for (prefix in USERNAME_FIELDS) {
- if (line.startsWith(prefix, ignoreCase = true)) return line.substring(prefix.length).trimStart()
- }
- }
- return null
- }
-
- private fun findAndStripPassword(passContent: List<String>): Pair<String, List<String>> {
- if (UriTotpFinder.TOTP_FIELDS.any { passContent[0].startsWith(it) }) return Pair("", passContent)
- for (line in passContent) {
- for (prefix in PASSWORD_FIELDS) {
- if (line.startsWith(prefix, ignoreCase = true)) {
- return Pair(line.substring(prefix.length).trimStart(), passContent.minus(line))
- }
- }
- }
- return Pair(passContent[0], passContent.minus(passContent[0]))
- }
-
- private fun findTotpSecret(decryptedContent: String): String? {
- return totpFinder.findSecret(decryptedContent)
- }
-
- private fun findOtpDigits(decryptedContent: String): String {
- return totpFinder.findDigits(decryptedContent)
- }
-
- private fun findTotpPeriod(decryptedContent: String): Long {
- return totpFinder.findPeriod(decryptedContent)
- }
-
- private fun findTotpAlgorithm(decryptedContent: String): String {
- return totpFinder.findAlgorithm(decryptedContent)
- }
-
- companion object {
-
- private const val EXTRA_CONTENT = "Extra Content"
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- val USERNAME_FIELDS =
- arrayOf(
- "login:",
- "username:",
- "user:",
- "account:",
- "email:",
- "name:",
- "handle:",
- "id:",
- "identity:",
- )
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- val PASSWORD_FIELDS =
- arrayOf(
- "password:",
- "secret:",
- "pass:",
- )
- }
-}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt
index ab1f402d..7a36899b 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt
@@ -16,6 +16,7 @@ import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e
import com.github.androidpasswordstore.autofillparser.AutofillAction
@@ -24,7 +25,8 @@ import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.onSuccess
import com.github.michaelbull.result.runCatching
-import dev.msfjarvis.aps.data.password.PasswordEntry
+import dagger.hilt.android.AndroidEntryPoint
+import dev.msfjarvis.aps.injection.password.PasswordEntryFactory
import dev.msfjarvis.aps.util.autofill.AutofillPreferences
import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder
import dev.msfjarvis.aps.util.autofill.DirectoryStructure
@@ -33,6 +35,7 @@ import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
+import javax.inject.Inject
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -49,6 +52,7 @@ import org.openintents.openpgp.IOpenPgpService2
import org.openintents.openpgp.OpenPgpError
@RequiresApi(Build.VERSION_CODES.O)
+@AndroidEntryPoint
class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope {
companion object {
@@ -77,6 +81,8 @@ class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope {
}
}
+ @Inject lateinit var passwordEntryFactory: PasswordEntryFactory
+
private val decryptInteractionRequiredAction =
registerForActivityResult(StartIntentSenderForResult()) { result ->
if (continueAfterUserInteraction != null) {
@@ -183,7 +189,8 @@ class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope {
runCatching {
val entry =
withContext(Dispatchers.IO) {
- @Suppress("BlockingMethodInNonBlockingContext") (PasswordEntry(decryptedOutput))
+ @Suppress("BlockingMethodInNonBlockingContext")
+ passwordEntryFactory.create(lifecycleScope, decryptedOutput.toByteArray())
}
AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure)
}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt
index fe688b40..f50410d4 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt
@@ -16,28 +16,34 @@ import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
+import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.aps.R
+import dev.msfjarvis.aps.data.passfile.PasswordEntry
import dev.msfjarvis.aps.data.password.FieldItem
-import dev.msfjarvis.aps.data.password.PasswordEntry
import dev.msfjarvis.aps.databinding.DecryptLayoutBinding
+import dev.msfjarvis.aps.injection.password.PasswordEntryFactory
import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter
import dev.msfjarvis.aps.util.extensions.viewBinding
import dev.msfjarvis.aps.util.settings.PreferenceKeys
import java.io.ByteArrayOutputStream
import java.io.File
+import javax.inject.Inject
import kotlin.time.ExperimentalTime
import kotlin.time.seconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.msfjarvis.openpgpktx.util.OpenPgpApi
import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
import org.openintents.openpgp.IOpenPgpService2
+@AndroidEntryPoint
class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
private val binding by viewBinding(DecryptLayoutBinding::inflate)
+ @Inject lateinit var passwordEntryFactory: PasswordEntryFactory
private val relativeParentPath by lazy(LazyThreadSafetyMode.NONE) { getParentPath(fullPath, repoPath) }
private var passwordEntry: PasswordEntry? = null
@@ -85,7 +91,7 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
passwordEntry?.let { entry ->
if (menu != null) {
menu.findItem(R.id.edit_password).isVisible = true
- if (entry.password.isNotEmpty()) {
+ if (entry.password.isNullOrBlank()) {
menu.findItem(R.id.share_password_as_plaintext).isVisible = true
menu.findItem(R.id.copy_password).isVisible = true
}
@@ -136,7 +142,7 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
intent.putExtra("REPO_PATH", repoPath)
intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name)
intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password)
- intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContent)
+ intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContentWithoutAuthData)
intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true)
startActivity(intent)
finish()
@@ -172,7 +178,7 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
startAutoDismissTimer()
runCatching {
val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true)
- val entry = PasswordEntry(outputStream)
+ val entry = passwordEntryFactory.create(lifecycleScope, outputStream.toByteArray())
val items = arrayListOf<FieldItem>()
val adapter = FieldItemAdapter(emptyList(), showPassword) { text -> copyTextToClipboard(text) }
@@ -183,37 +189,25 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
passwordEntry = entry
invalidateOptionsMenu()
- if (entry.password.isNotEmpty()) {
- items.add(FieldItem.createPasswordField(entry.password))
+ if (entry.password.isNullOrBlank()) {
+ items.add(FieldItem.createPasswordField(entry.password!!))
}
if (entry.hasTotp()) {
launch(Dispatchers.IO) {
- // Calculate the actual remaining time for the first pass
- // then return to the standard rotation.
- val remainingTime = entry.totpPeriod - (System.currentTimeMillis() % entry.totpPeriod)
withContext(Dispatchers.Main) {
- val code = entry.calculateTotpCode() ?: "Error"
+ val code = entry.totp.value
items.add(FieldItem.createOtpField(code))
}
- delay(remainingTime.seconds)
- repeat(Int.MAX_VALUE) {
- val code = entry.calculateTotpCode() ?: "Error"
- withContext(Dispatchers.Main) { adapter.updateOTPCode(code) }
- delay(entry.totpPeriod.seconds)
- }
+ entry.totp.collect { code -> withContext(Dispatchers.Main) { adapter.updateOTPCode(code) } }
}
}
- if (!entry.username.isNullOrEmpty()) {
- items.add(FieldItem.createUsernameField(entry.username))
+ if (!entry.username.isNullOrBlank()) {
+ items.add(FieldItem.createUsernameField(entry.username!!))
}
- if (entry.hasExtraContentWithoutAuthData()) {
- entry.extraContentMap.forEach { (key, value) ->
- items.add(FieldItem(key, value, FieldItem.ActionType.COPY))
- }
- }
+ entry.extraContent.forEach { (key, value) -> items.add(FieldItem(key, value, FieldItem.ActionType.COPY)) }
binding.recyclerView.adapter = adapter
adapter.updateItems(items)
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt
index 7966628f..462dc388 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt
@@ -28,10 +28,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.zxing.integration.android.IntentIntegrator
import com.google.zxing.integration.android.IntentIntegrator.QR_CODE
+import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.aps.R
-import dev.msfjarvis.aps.data.password.PasswordEntry
import dev.msfjarvis.aps.data.repo.PasswordRepository
import dev.msfjarvis.aps.databinding.PasswordCreationActivityBinding
+import dev.msfjarvis.aps.injection.password.PasswordEntryFactory
import dev.msfjarvis.aps.ui.dialogs.OtpImportDialogFragment
import dev.msfjarvis.aps.ui.dialogs.PasswordGeneratorDialogFragment
import dev.msfjarvis.aps.ui.dialogs.XkPasswordGeneratorDialogFragment
@@ -49,15 +50,18 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
+import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.msfjarvis.openpgpktx.util.OpenPgpApi
import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
+@AndroidEntryPoint
class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
+ @Inject lateinit var passwordEntryFactory: PasswordEntryFactory
private val suggestedName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) }
private val suggestedPass by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_PASSWORD) }
@@ -221,7 +225,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
} else {
// User wants to disable username encryption, so we extract the
// username from the encrypted extras and use it as the filename.
- val entry = PasswordEntry("PASSWORD\n${extraContent.text}")
+ val entry =
+ passwordEntryFactory.create(lifecycleScope, "PASSWORD\n${extraContent.text}".encodeToByteArray())
val username = entry.username
// username should not be null here by the logic in
@@ -288,11 +293,11 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
private fun updateViewState() =
with(binding) {
// Use PasswordEntry to parse extras for username
- val entry = PasswordEntry("PLACEHOLDER\n${extraContent.text}")
+ val entry = passwordEntryFactory.create(lifecycleScope, "PLACEHOLDER\n${extraContent.text}".encodeToByteArray())
encryptUsername.apply {
if (visibility != View.VISIBLE) return@apply
val hasUsernameInFileName = filename.text.toString().isNotBlank()
- val hasUsernameInExtras = entry.hasUsername()
+ val hasUsernameInExtras = !entry.username.isNullOrBlank()
isEnabled = hasUsernameInFileName xor hasUsernameInExtras
isChecked = hasUsernameInExtras
}
@@ -430,7 +435,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
if (shouldGeneratePassword) {
val directoryStructure = AutofillPreferences.directoryStructure(applicationContext)
- val entry = PasswordEntry(content)
+ val entry = passwordEntryFactory.create(lifecycleScope, content.encodeToByteArray())
returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password)
val username = entry.username ?: directoryStructure.getUsernameFor(file)
returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/autofill/AutofillPreferences.kt b/app/src/main/java/dev/msfjarvis/aps/util/autofill/AutofillPreferences.kt
index 6e1fe464..c6cdffed 100644
--- a/app/src/main/java/dev/msfjarvis/aps/util/autofill/AutofillPreferences.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/util/autofill/AutofillPreferences.kt
@@ -8,7 +8,7 @@ import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import com.github.androidpasswordstore.autofillparser.Credentials
-import dev.msfjarvis.aps.data.password.PasswordEntry
+import dev.msfjarvis.aps.data.passfile.PasswordEntry
import dev.msfjarvis.aps.util.extensions.getString
import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.services.getDefaultUsername
@@ -139,6 +139,6 @@ object AutofillPreferences {
): Credentials {
// Always give priority to a username stored in the encrypted extras
val username = entry.username ?: directoryStructure.getUsernameFor(file) ?: context.getDefaultUsername()
- return Credentials(username, entry.password, entry.calculateTotpCode())
+ return Credentials(username, entry.password, entry.totp.value)
}
}
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
deleted file mode 100644
index 1ef155a5..00000000
--- a/app/src/main/java/dev/msfjarvis/aps/util/totp/Otp.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package dev.msfjarvis.aps.util.totp
-
-import com.github.michaelbull.result.Err
-import com.github.michaelbull.result.runCatching
-import java.nio.ByteBuffer
-import java.util.Locale
-import javax.crypto.Mac
-import javax.crypto.spec.SecretKeySpec
-import kotlin.experimental.and
-import org.apache.commons.codec.binary.Base32
-
-object Otp {
-
- private val BASE_32 = Base32()
- private val STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY".toCharArray()
-
- 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())
- }
- // 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)
- }
- }
- }
- }
-}
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
deleted file mode 100644
index e787fea5..00000000
--- a/app/src/main/java/dev/msfjarvis/aps/util/totp/TotpFinder.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package dev.msfjarvis.aps.util.totp
-
-/** 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 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 algorithm for the TOTP secret. */
- fun findAlgorithm(content: String): String
-}
diff --git a/app/src/test/java/dev/msfjarvis/aps/data/password/PasswordEntryTest.kt b/app/src/test/java/dev/msfjarvis/aps/data/password/PasswordEntryTest.kt
deleted file mode 100644
index afbe9289..00000000
--- a/app/src/test/java/dev/msfjarvis/aps/data/password/PasswordEntryTest.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package dev.msfjarvis.aps.data.password
-
-import com.github.michaelbull.result.get
-import dev.msfjarvis.aps.util.totp.Otp
-import dev.msfjarvis.aps.util.totp.TotpFinder
-import java.util.Date
-import kotlin.test.assertEquals
-import kotlin.test.assertFalse
-import kotlin.test.assertNotNull
-import kotlin.test.assertNull
-import kotlin.test.assertTrue
-import org.junit.Test
-
-class PasswordEntryTest {
-
- private fun makeEntry(content: String) = PasswordEntry(content, testFinder)
-
- @Test
- fun testGetPassword() {
- assertEquals("fooooo", makeEntry("fooooo\nbla\n").password)
- assertEquals("fooooo", makeEntry("fooooo\nbla").password)
- assertEquals("fooooo", makeEntry("fooooo\n").password)
- assertEquals("fooooo", makeEntry("fooooo").password)
- assertEquals("", makeEntry("\nblubb\n").password)
- assertEquals("", makeEntry("\nblubb").password)
- assertEquals("", makeEntry("\n").password)
- assertEquals("", makeEntry("").password)
- for (field in PasswordEntry.PASSWORD_FIELDS) {
- assertEquals("fooooo", makeEntry("\n$field fooooo").password)
- assertEquals("fooooo", makeEntry("\n${field.toUpperCase()} fooooo").password)
- assertEquals("fooooo", makeEntry("GOPASS-SECRET-1.0\n$field fooooo").password)
- assertEquals("fooooo", makeEntry("someFirstLine\nUsername: bar\n$field fooooo").password)
- }
- }
-
- @Test
- fun testGetExtraContent() {
- assertEquals("bla\n", makeEntry("fooooo\nbla\n").extraContent)
- assertEquals("bla", makeEntry("fooooo\nbla").extraContent)
- assertEquals("", makeEntry("fooooo\n").extraContent)
- assertEquals("", makeEntry("fooooo").extraContent)
- assertEquals("blubb\n", makeEntry("\nblubb\n").extraContent)
- assertEquals("blubb", makeEntry("\nblubb").extraContent)
- assertEquals("blubb", makeEntry("blubb\npassword: foo").extraContent)
- assertEquals("blubb", makeEntry("password: foo\nblubb").extraContent)
- assertEquals("blubb\nusername: bar", makeEntry("blubb\npassword: foo\nusername: bar").extraContent)
- assertEquals("", makeEntry("\n").extraContent)
- assertEquals("", makeEntry("").extraContent)
- }
-
- @Test
- fun parseExtraContentWithoutAuth() {
- var entry = makeEntry("username: abc\npassword: abc\ntest: abcdef")
- assertEquals(1, entry.extraContentMap.size)
- assertTrue(entry.extraContentMap.containsKey("test"))
- assertEquals("abcdef", entry.extraContentMap["test"])
-
- entry = makeEntry("username: abc\npassword: abc\ntest: :abcdef:")
- assertEquals(1, entry.extraContentMap.size)
- assertTrue(entry.extraContentMap.containsKey("test"))
- assertEquals(":abcdef:", entry.extraContentMap["test"])
-
- entry = makeEntry("username: abc\npassword: abc\ntest : ::abc:def::")
- assertEquals(1, entry.extraContentMap.size)
- assertTrue(entry.extraContentMap.containsKey("test"))
- assertEquals("::abc:def::", entry.extraContentMap["test"])
-
- entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\ntest2: ghijkl")
- assertEquals(2, entry.extraContentMap.size)
- assertTrue(entry.extraContentMap.containsKey("test2"))
- assertEquals("ghijkl", entry.extraContentMap["test2"])
-
- entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\n: ghijkl\n mnopqr:")
- assertEquals(2, entry.extraContentMap.size)
- assertTrue(entry.extraContentMap.containsKey("Extra Content"))
- assertEquals(": ghijkl\n mnopqr:", entry.extraContentMap["Extra Content"])
-
- entry = makeEntry("username: abc\npassword: abc\n:\n\n")
- assertEquals(1, entry.extraContentMap.size)
- assertTrue(entry.extraContentMap.containsKey("Extra Content"))
- assertEquals(":", entry.extraContentMap["Extra Content"])
- }
-
- @Test
- fun testGetUsername() {
- for (field in PasswordEntry.USERNAME_FIELDS) {
- assertEquals("username", makeEntry("\n$field username").username)
- assertEquals("username", makeEntry("\n${field.toUpperCase()} username").username)
- }
- assertEquals("username", makeEntry("secret\nextra\nlogin: username\ncontent\n").username)
- assertEquals("username", makeEntry("\nextra\nusername: username\ncontent\n").username)
- assertEquals("username", makeEntry("\nUSERNaMe: username\ncontent\n").username)
- assertEquals("username", makeEntry("\nlogin: username").username)
- assertEquals("foo@example.com", makeEntry("\nemail: foo@example.com").username)
- assertEquals("username", makeEntry("\nidentity: username\nlogin: another_username").username)
- assertEquals("username", makeEntry("\nLOGiN:username").username)
- assertNull(makeEntry("secret\nextra\ncontent\n").username)
- }
-
- @Test
- fun testHasUsername() {
- assertTrue(makeEntry("secret\nextra\nlogin: username\ncontent\n").hasUsername())
- assertFalse(makeEntry("secret\nextra\ncontent\n").hasUsername())
- assertFalse(makeEntry("secret\nlogin failed\n").hasUsername())
- assertFalse(makeEntry("\n").hasUsername())
- assertFalse(makeEntry("").hasUsername())
- }
-
- @Test
- fun testGeneratesOtpFromTotpUri() {
- val entry = makeEntry("secret\nextra\n$TOTP_URI")
- assertTrue(entry.hasTotp())
- val code =
- Otp.calculateCode(
- entry.totpSecret!!,
- // The hardcoded date value allows this test to stay reproducible.
- Date(8640000).time / (1000 * entry.totpPeriod),
- entry.totpAlgorithm,
- entry.digits
- )
- .get()
- assertNotNull(code) { "Generated OTP cannot be null" }
- assertEquals(entry.digits.toInt(), code.length)
- assertEquals("545293", code)
- }
-
- @Test
- fun testGeneratesOtpWithOnlyUriInFile() {
- val entry = makeEntry(TOTP_URI)
- assertTrue(entry.password.isEmpty())
- assertTrue(entry.hasTotp())
- val code =
- Otp.calculateCode(
- entry.totpSecret!!,
- // The hardcoded date value allows this test to stay reproducible.
- Date(8640000).time / (1000 * entry.totpPeriod),
- entry.totpAlgorithm,
- entry.digits
- )
- .get()
- assertNotNull(code) { "Generated OTP cannot be null" }
- assertEquals(entry.digits.toInt(), code.length)
- assertEquals("545293", code)
- }
-
- @Test
- fun testOnlyLooksForUriInFirstLine() {
- val entry = makeEntry("id:\n$TOTP_URI")
- assertTrue(entry.password.isNotEmpty())
- assertTrue(entry.hasTotp())
- assertFalse(entry.hasUsername())
- }
-
- // https://github.com/android-password-store/Android-Password-Store/issues/1190
- @Test
- fun extraContentWithMultipleUsernameFields() {
- val entry = makeEntry("pass\nuser: user\nid: id\n$TOTP_URI")
- assertTrue(entry.hasExtraContent())
- assertTrue(entry.hasTotp())
- assertTrue(entry.hasUsername())
- assertEquals("pass", entry.password)
- assertEquals("user", entry.username)
- assertEquals("id: id", entry.extraContentWithoutAuthData)
- }
-
- companion object {
-
- const val TOTP_URI =
- "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"
-
- // This implementation is hardcoded for the URI above.
- val testFinder =
- object : TotpFinder {
- override fun findSecret(content: String): String {
- return "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"
- }
-
- override fun findDigits(content: String): String {
- return "6"
- }
-
- override fun findPeriod(content: String): Long {
- return 30
- }
-
- override fun findAlgorithm(content: String): String {
- return "SHA1"
- }
- }
- }
-}
diff --git a/app/src/test/java/dev/msfjarvis/aps/util/totp/OtpTest.kt b/app/src/test/java/dev/msfjarvis/aps/util/totp/OtpTest.kt
deleted file mode 100644
index d41c3be9..00000000
--- a/app/src/test/java/dev/msfjarvis/aps/util/totp/OtpTest.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package dev.msfjarvis.aps.util.totp
-
-import com.github.michaelbull.result.get
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
-import kotlin.test.assertNull
-import org.junit.Test
-
-class OtpTest {
-
- @Test
- fun testOtpGeneration6Digits() {
- assertEquals("953550", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333298159 / (1000 * 30), "SHA1", "6").get())
- assertEquals("275379", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333571918 / (1000 * 30), "SHA1", "6").get())
- assertEquals("867507", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333600517 / (1000 * 57), "SHA1", "6").get())
- }
-
- @Test
- fun testOtpGeneration10Digits() {
- assertEquals("0740900914", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333655044 / (1000 * 30), "SHA1", "10").get())
- assertEquals("0070632029", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333691405 / (1000 * 30), "SHA1", "10").get())
- assertEquals("1017265882", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333728893 / (1000 * 83), "SHA1", "10").get())
- }
-
- @Test
- fun testOtpGenerationIllegalInput() {
- assertNull(Otp.calculateCode("JBSWY3DPEHPK3PXP", 10000, "SHA0", "10").get())
- assertNull(Otp.calculateCode("JBSWY3DPEHPK3PXP", 10000, "SHA1", "a").get())
- assertNull(Otp.calculateCode("JBSWY3DPEHPK3PXP", 10000, "SHA1", "5").get())
- assertNull(Otp.calculateCode("JBSWY3DPEHPK3PXP", 10000, "SHA1", "11").get())
- assertNull(Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAB", 10000, "SHA1", "6").get())
- }
-
- @Test
- fun testOtpGenerationUnusualSecrets() {
- assertEquals(
- "127764",
- Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAAAAA", 1593367111963 / (1000 * 30), "SHA1", "6").get()
- )
- assertEquals("047515", Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAA", 1593367171420 / (1000 * 30), "SHA1", "6").get())
- }
-
- @Test
- fun testOtpGenerationUnpaddedSecrets() {
- // Secret was generated with `echo 'string with some padding needed' | base32`
- // We don't care for the resultant OTP's actual value, we just want both the padded and
- // unpadded variant to generate the same one.
- val unpaddedOtp =
- Otp.calculateCode(
- "ON2HE2LOM4QHO2LUNAQHG33NMUQHAYLEMRUW4ZZANZSWKZDFMQFA",
- 1593367171420 / (1000 * 30),
- "SHA1",
- "6"
- )
- .get()
- val paddedOtp =
- Otp.calculateCode(
- "ON2HE2LOM4QHO2LUNAQHG33NMUQHAYLEMRUW4ZZANZSWKZDFMQFA====",
- 1593367171420 / (1000 * 30),
- "SHA1",
- "6"
- )
- .get()
- assertNotNull(unpaddedOtp)
- assertNotNull(paddedOtp)
- assertEquals(unpaddedOtp, paddedOtp)
- }
-}