diff options
author | Harsh Shandilya <me@msfjarvis.dev> | 2021-02-18 12:17:03 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-18 12:17:03 +0530 |
commit | 92ece7dbb5607258bcf954963009bf1f9411ab07 (patch) | |
tree | 3d9f3208e9d7f4ebbd996b1f4b99bec86713d51e /app/src/main | |
parent | 051d455c9f68d7edbc75abbc8d9293dd34d1d250 (diff) |
Implement manual TOTP import and cleanup password generators (#1320)
Diffstat (limited to 'app/src/main')
6 files changed, 170 insertions, 48 deletions
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> |