From 92ece7dbb5607258bcf954963009bf1f9411ab07 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Thu, 18 Feb 2021 12:17:03 +0530 Subject: [PATCH] Implement manual TOTP import and cleanup password generators (#1320) --- CHANGELOG.md | 1 + .../aps/ui/crypto/PasswordCreationActivity.kt | 39 +++++++++++++--- .../aps/ui/dialogs/OtpImportDialogFragment.kt | 46 +++++++++++++++++++ .../PasswordGeneratorDialogFragment.kt | 45 +++++++++--------- .../XkPasswordGeneratorDialogFragment.kt | 38 ++++++++------- .../res/layout/fragment_manual_otp_entry.xml | 46 +++++++++++++++++++ app/src/main/res/values/strings.xml | 4 ++ 7 files changed, 171 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/dialogs/OtpImportDialogFragment.kt create mode 100644 app/src/main/res/layout/fragment_manual_otp_entry.xml 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(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(R.id.numerals)?.isChecked = !prefs.getBoolean(PasswordOption.NoDigits.key, false) - view.findViewById(R.id.symbols)?.isChecked = prefs.getBoolean(PasswordOption.AtLeastOneSymbol.key, false) - view.findViewById(R.id.uppercase)?.isChecked = !prefs.getBoolean(PasswordOption.NoUppercaseLetters.key, false) - view.findViewById(R.id.lowercase)?.isChecked = !prefs.getBoolean(PasswordOption.NoLowercaseLetters.key, false) - view.findViewById(R.id.ambiguous)?.isChecked = !prefs.getBoolean(PasswordOption.NoAmbiguousCharacters.key, false) - view.findViewById(R.id.pronounceable)?.isChecked = !prefs.getBoolean(PasswordOption.FullyRandom.key, true) + builder.setView(binding.root) - 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.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) + + 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(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(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 @@ + + + + + + + + + + + + + + + + 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 @@ Fill and save passwords (saving requires that no accessibility services are enabled) Clear saved host key Successfully cleared saved host key! + Scan QR code + Enter manually + Secret + Account