aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2021-02-18 12:17:03 +0530
committerGitHub <noreply@github.com>2021-02-18 12:17:03 +0530
commit92ece7dbb5607258bcf954963009bf1f9411ab07 (patch)
tree3d9f3208e9d7f4ebbd996b1f4b99bec86713d51e
parent051d455c9f68d7edbc75abbc8d9293dd34d1d250 (diff)
Implement manual TOTP import and cleanup password generators (#1320)
-rw-r--r--CHANGELOG.md1
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt39
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/dialogs/OtpImportDialogFragment.kt46
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt45
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/dialogs/XkPasswordGeneratorDialogFragment.kt38
-rw-r--r--app/src/main/res/layout/fragment_manual_otp_entry.xml46
-rw-r--r--app/src/main/res/values/strings.xml4
7 files changed, 171 insertions, 48 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a8a7c80..cb811fc8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
- Invalid `.gpg-id` files can now be fixed automatically by deleting them and then trying to create a new password.
- Suggest users to re-clone repository when it is deemed to be broken
- Allow doing a merge instead of a rebase when pulling or syncing
+- Add support for manually providing TOTP parameters
### Fixed
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 749ff741..dfde4845 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
@@ -30,6 +30,7 @@ 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.ui.dialogs.OtpImportDialogFragment
import dev.msfjarvis.aps.ui.dialogs.PasswordGeneratorDialogFragment
import dev.msfjarvis.aps.ui.dialogs.XkPasswordGeneratorDialogFragment
import dev.msfjarvis.aps.util.autofill.AutofillPreferences
@@ -145,12 +146,30 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
setContentView(root)
generatePassword.setOnClickListener { generatePassword() }
otpImportButton.setOnClickListener {
- otpImportAction.launch(IntentIntegrator(this@PasswordCreationActivity)
- .setOrientationLocked(false)
- .setBeepEnabled(false)
- .setDesiredBarcodeFormats(QR_CODE)
- .createScanIntent()
- )
+ supportFragmentManager.setFragmentResultListener(OTP_RESULT_REQUEST_KEY, this@PasswordCreationActivity) { requestKey, bundle ->
+ if (requestKey == OTP_RESULT_REQUEST_KEY) {
+ val contents = bundle.getString(RESULT)
+ val currentExtras = binding.extraContent.text.toString()
+ if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
+ binding.extraContent.append("\n$contents")
+ else
+ binding.extraContent.append(contents)
+ }
+ }
+ val items = arrayOf(getString(R.string.otp_import_qr_code), getString(R.string.otp_import_manual_entry))
+ MaterialAlertDialogBuilder(this@PasswordCreationActivity)
+ .setItems(items) { _, index ->
+ if (index == 0) {
+ otpImportAction.launch(IntentIntegrator(this@PasswordCreationActivity)
+ .setOrientationLocked(false)
+ .setBeepEnabled(false)
+ .setDesiredBarcodeFormats(QR_CODE)
+ .createScanIntent())
+ } else if (index == 1) {
+ OtpImportDialogFragment().show(supportFragmentManager, "OtpImport")
+ }
+ }
+ .show()
}
directoryInputLayout.apply {
@@ -249,6 +268,11 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
}
private fun generatePassword() {
+ supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) { requestKey, bundle ->
+ if (requestKey == PASSWORD_RESULT_REQUEST_KEY) {
+ binding.password.setText(bundle.getString(RESULT))
+ }
+ }
when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) {
KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment()
.show(supportFragmentManager, "generator")
@@ -467,6 +491,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
private const val KEY_PWGEN_TYPE_CLASSIC = "classic"
private const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd"
+ const val PASSWORD_RESULT_REQUEST_KEY = "PASSWORD_GENERATOR"
+ const val OTP_RESULT_REQUEST_KEY = "OTP_IMPORT"
+ const val RESULT = "RESULT"
const val RETURN_EXTRA_CREATED_FILE = "CREATED_FILE"
const val RETURN_EXTRA_NAME = "NAME"
const val RETURN_EXTRA_LONG_NAME = "LONG_NAME"
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/OtpImportDialogFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/OtpImportDialogFragment.kt
new file mode 100644
index 00000000..4389b1c1
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/OtpImportDialogFragment.kt
@@ -0,0 +1,46 @@
+package dev.msfjarvis.aps.ui.dialogs
+
+import android.app.Dialog
+import android.net.Uri
+import android.os.Bundle
+import androidx.core.os.bundleOf
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.setFragmentResult
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.textfield.TextInputEditText
+import dev.msfjarvis.aps.R
+import dev.msfjarvis.aps.databinding.FragmentManualOtpEntryBinding
+import dev.msfjarvis.aps.ui.crypto.PasswordCreationActivity
+import dev.msfjarvis.aps.util.extensions.requestInputFocusOnView
+
+class OtpImportDialogFragment : DialogFragment() {
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = MaterialAlertDialogBuilder(requireContext())
+ val binding = FragmentManualOtpEntryBinding.inflate(layoutInflater)
+ builder.setView(binding.root)
+ builder.setPositiveButton(android.R.string.ok) { _, _ ->
+ setFragmentResult(
+ PasswordCreationActivity.OTP_RESULT_REQUEST_KEY,
+ bundleOf(
+ PasswordCreationActivity.RESULT to getTOTPUri(binding)
+ )
+ )
+ }
+ val dialog = builder.create()
+ dialog.requestInputFocusOnView<TextInputEditText>(R.id.secret)
+ return dialog
+ }
+
+ private fun getTOTPUri(binding: FragmentManualOtpEntryBinding): String {
+ val secret = binding.secret.text.toString()
+ val account = binding.account.text.toString()
+ if (secret.isBlank()) return ""
+ val builder = Uri.Builder()
+ builder.scheme("otpauth")
+ builder.authority("totp")
+ builder.appendQueryParameter("secret", secret)
+ if (account.isNotBlank()) builder.appendQueryParameter("issuer", account)
+ return builder.build().toString()
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt
index 0676ef1c..33f8b899 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt
@@ -4,7 +4,6 @@
*/
package dev.msfjarvis.aps.ui.dialogs
-import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
@@ -14,13 +13,16 @@ import android.widget.CheckBox
import android.widget.EditText
import android.widget.Toast
import androidx.annotation.IdRes
-import androidx.appcompat.widget.AppCompatEditText
import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.setFragmentResult
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.runCatching
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.msfjarvis.aps.R
+import dev.msfjarvis.aps.databinding.FragmentPwgenBinding
+import dev.msfjarvis.aps.ui.crypto.PasswordCreationActivity
import dev.msfjarvis.aps.util.pwgen.PasswordGenerator
import dev.msfjarvis.aps.util.pwgen.PasswordGenerator.generate
import dev.msfjarvis.aps.util.pwgen.PasswordGenerator.setPrefs
@@ -30,41 +32,40 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys
class PasswordGeneratorDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = MaterialAlertDialogBuilder(requireContext())
val callingActivity = requireActivity()
- val inflater = callingActivity.layoutInflater
-
- @SuppressLint("InflateParams")
- val view = inflater.inflate(R.layout.fragment_pwgen, null)
+ val binding = FragmentPwgenBinding.inflate(layoutInflater)
val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf")
val prefs = requireActivity().applicationContext
.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
- view.findViewById<CheckBox>(R.id.numerals)?.isChecked = !prefs.getBoolean(PasswordOption.NoDigits.key, false)
- view.findViewById<CheckBox>(R.id.symbols)?.isChecked = prefs.getBoolean(PasswordOption.AtLeastOneSymbol.key, false)
- view.findViewById<CheckBox>(R.id.uppercase)?.isChecked = !prefs.getBoolean(PasswordOption.NoUppercaseLetters.key, false)
- view.findViewById<CheckBox>(R.id.lowercase)?.isChecked = !prefs.getBoolean(PasswordOption.NoLowercaseLetters.key, false)
- view.findViewById<CheckBox>(R.id.ambiguous)?.isChecked = !prefs.getBoolean(PasswordOption.NoAmbiguousCharacters.key, false)
- view.findViewById<CheckBox>(R.id.pronounceable)?.isChecked = !prefs.getBoolean(PasswordOption.FullyRandom.key, true)
+ builder.setView(binding.root)
+
+ binding.numerals.isChecked = !prefs.getBoolean(PasswordOption.NoDigits.key, false)
+ binding.symbols.isChecked = prefs.getBoolean(PasswordOption.AtLeastOneSymbol.key, false)
+ binding.uppercase.isChecked = !prefs.getBoolean(PasswordOption.NoUppercaseLetters.key, false)
+ binding.lowercase.isChecked = !prefs.getBoolean(PasswordOption.NoLowercaseLetters.key, false)
+ binding.ambiguous.isChecked = !prefs.getBoolean(PasswordOption.NoAmbiguousCharacters.key, false)
+ binding.pronounceable.isChecked = !prefs.getBoolean(PasswordOption.FullyRandom.key, true)
- val textView: AppCompatEditText = view.findViewById(R.id.lengthNumber)
- textView.setText(prefs.getInt(PreferenceKeys.LENGTH, 20).toString())
- val passwordText: AppCompatTextView = view.findViewById(R.id.passwordText)
- passwordText.typeface = monoTypeface
- return MaterialAlertDialogBuilder(requireContext()).run {
+ binding.lengthNumber.setText(prefs.getInt(PreferenceKeys.LENGTH, 20).toString())
+ binding.passwordText.typeface = monoTypeface
+ return builder.run {
setTitle(R.string.pwgen_title)
- setView(view)
setPositiveButton(R.string.dialog_ok) { _, _ ->
- val edit = callingActivity.findViewById<EditText>(R.id.password)
- edit.setText(passwordText.text)
+ setFragmentResult(
+ PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY,
+ bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}")
+ )
}
setNeutralButton(R.string.dialog_cancel) { _, _ -> }
setNegativeButton(R.string.pwgen_generate, null)
create()
}.apply {
setOnShowListener {
- generate(passwordText)
+ generate(binding.passwordText)
getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
- generate(passwordText)
+ generate(binding.passwordText)
}
}
}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/XkPasswordGeneratorDialogFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/XkPasswordGeneratorDialogFragment.kt
index 743452b8..53c7a68b 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/XkPasswordGeneratorDialogFragment.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/XkPasswordGeneratorDialogFragment.kt
@@ -9,12 +9,12 @@ import android.content.Context
import android.content.SharedPreferences
import android.graphics.Typeface
import android.os.Bundle
-import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.edit
+import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.setFragmentResult
import com.github.ajalt.timberkt.Timber.tag
import com.github.michaelbull.result.fold
import com.github.michaelbull.result.getOr
@@ -22,6 +22,7 @@ import com.github.michaelbull.result.runCatching
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.databinding.FragmentXkpwgenBinding
+import dev.msfjarvis.aps.ui.crypto.PasswordCreationActivity
import dev.msfjarvis.aps.util.extensions.getString
import dev.msfjarvis.aps.util.pwgenxkpwd.CapsType
import dev.msfjarvis.aps.util.pwgenxkpwd.PasswordBuilder
@@ -29,21 +30,16 @@ import dev.msfjarvis.aps.util.pwgenxkpwd.PasswordBuilder
/** A placeholder fragment containing a simple view. */
class XkPasswordGeneratorDialogFragment : DialogFragment() {
- private lateinit var prefs: SharedPreferences
- private lateinit var binding: FragmentXkpwgenBinding
-
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = MaterialAlertDialogBuilder(requireContext())
val callingActivity = requireActivity()
val inflater = callingActivity.layoutInflater
- binding = FragmentXkpwgenBinding.inflate(inflater)
-
+ val binding = FragmentXkpwgenBinding.inflate(inflater)
val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf")
+ val prefs = callingActivity.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
builder.setView(binding.root)
- prefs = callingActivity.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
-
val previousStoredCapStyle: String = runCatching {
prefs.getString(PREF_KEY_CAPITALS_STYLE)!!
}.getOr(DEFAULT_CAPS_STYLE)
@@ -60,9 +56,11 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
binding.xkPasswordText.typeface = monoTypeface
builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ ->
- setPreferences()
- val edit = callingActivity.findViewById<EditText>(R.id.password)
- edit.setText(binding.xkPasswordText.text)
+ setPreferences(binding, prefs)
+ setFragmentResult(
+ PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY,
+ bundleOf(PasswordCreationActivity.RESULT to "${binding.xkPasswordText.text}")
+ )
}
// flip neutral and negative buttons
@@ -72,18 +70,18 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
val dialog = builder.setTitle(this.resources.getString(R.string.xkpwgen_title)).create()
dialog.setOnShowListener {
- setPreferences()
- makeAndSetPassword(binding.xkPasswordText)
+ setPreferences(binding, prefs)
+ makeAndSetPassword(binding)
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
- setPreferences()
- makeAndSetPassword(binding.xkPasswordText)
+ setPreferences(binding, prefs)
+ makeAndSetPassword(binding)
}
}
return dialog
}
- private fun makeAndSetPassword(passwordText: AppCompatTextView) {
+ private fun makeAndSetPassword(binding: FragmentXkpwgenBinding) {
PasswordBuilder(requireContext())
.setNumberOfWords(Integer.valueOf(binding.xkNumWords.text.toString()))
.setMinimumWordLength(DEFAULT_MIN_WORD_LENGTH)
@@ -93,16 +91,16 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
.appendSymbols(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_SYMBOL })
.setCapitalization(CapsType.valueOf(binding.xkCapType.selectedItem.toString())).create()
.fold(
- success = { passwordText.text = it },
+ success = { binding.xkPasswordText.text = it },
failure = { e ->
Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show()
tag("xkpw").e(e, "failure generating xkpasswd")
- passwordText.text = FALLBACK_ERROR_PASS
+ binding.xkPasswordText.text = FALLBACK_ERROR_PASS
},
)
}
- private fun setPreferences() {
+ private fun setPreferences(binding: FragmentXkpwgenBinding, prefs: SharedPreferences) {
prefs.edit {
putString(PREF_KEY_CAPITALS_STYLE, binding.xkCapType.selectedItem.toString())
putString(PREF_KEY_NUM_WORDS, binding.xkNumWords.text.toString())
diff --git a/app/src/main/res/layout/fragment_manual_otp_entry.xml b/app/src/main/res/layout/fragment_manual_otp_entry.xml
new file mode 100644
index 00000000..eef81dd5
--- /dev/null
+++ b/app/src/main/res/layout/fragment_manual_otp_entry.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/secret_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/otp_import_manual_hint_secret"
+ android:paddingStart="16dp"
+ android:paddingTop="16dp"
+ android:paddingEnd="16dp"
+ app:hintEnabled="true"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/secret"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/account_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/otp_import_manual_hint_account"
+ android:paddingStart="16dp"
+ android:paddingTop="16dp"
+ android:paddingEnd="16dp"
+ android:paddingBottom="16dp"
+ app:hintEnabled="true"
+ app:layout_constraintTop_toBottomOf="@id/secret_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/account"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 230be155..98e1da76 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -398,5 +398,9 @@
<string name="oreo_autofill_password_fill_and_conditional_save_support">Fill and save passwords (saving requires that no accessibility services are enabled)</string>
<string name="clear_saved_host_key">Clear saved host key</string>
<string name="clear_saved_host_key_success">Successfully cleared saved host key!</string>
+ <string name="otp_import_qr_code">Scan QR code</string>
+ <string name="otp_import_manual_entry">Enter manually</string>
+ <string name="otp_import_manual_hint_secret">Secret</string>
+ <string name="otp_import_manual_hint_account">Account</string>
</resources>