Implement manual TOTP import and cleanup password generators (#1320)

This commit is contained in:
Harsh Shandilya
2021-02-18 12:17:03 +05:30
committed by GitHub
parent 051d455c9f
commit 92ece7dbb5
7 changed files with 171 additions and 48 deletions

View File

@@ -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. - 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 - 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 - Allow doing a merge instead of a rebase when pulling or syncing
- Add support for manually providing TOTP parameters
### Fixed ### Fixed

View File

@@ -30,6 +30,7 @@ import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.data.password.PasswordEntry import dev.msfjarvis.aps.data.password.PasswordEntry
import dev.msfjarvis.aps.data.repo.PasswordRepository import dev.msfjarvis.aps.data.repo.PasswordRepository
import dev.msfjarvis.aps.databinding.PasswordCreationActivityBinding 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.PasswordGeneratorDialogFragment
import dev.msfjarvis.aps.ui.dialogs.XkPasswordGeneratorDialogFragment import dev.msfjarvis.aps.ui.dialogs.XkPasswordGeneratorDialogFragment
import dev.msfjarvis.aps.util.autofill.AutofillPreferences import dev.msfjarvis.aps.util.autofill.AutofillPreferences
@@ -145,12 +146,30 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
setContentView(root) setContentView(root)
generatePassword.setOnClickListener { generatePassword() } generatePassword.setOnClickListener { generatePassword() }
otpImportButton.setOnClickListener { otpImportButton.setOnClickListener {
otpImportAction.launch(IntentIntegrator(this@PasswordCreationActivity) supportFragmentManager.setFragmentResultListener(OTP_RESULT_REQUEST_KEY, this@PasswordCreationActivity) { requestKey, bundle ->
.setOrientationLocked(false) if (requestKey == OTP_RESULT_REQUEST_KEY) {
.setBeepEnabled(false) val contents = bundle.getString(RESULT)
.setDesiredBarcodeFormats(QR_CODE) val currentExtras = binding.extraContent.text.toString()
.createScanIntent() 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 { directoryInputLayout.apply {
@@ -249,6 +268,11 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
} }
private fun generatePassword() { 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) { when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) {
KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment() KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment()
.show(supportFragmentManager, "generator") .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_CLASSIC = "classic"
private const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd" 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_CREATED_FILE = "CREATED_FILE"
const val RETURN_EXTRA_NAME = "NAME" const val RETURN_EXTRA_NAME = "NAME"
const val RETURN_EXTRA_LONG_NAME = "LONG_NAME" const val RETURN_EXTRA_LONG_NAME = "LONG_NAME"

View File

@@ -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()
}
}

View File

@@ -4,7 +4,6 @@
*/ */
package dev.msfjarvis.aps.ui.dialogs package dev.msfjarvis.aps.ui.dialogs
import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
@@ -14,13 +13,16 @@ import android.widget.CheckBox
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.appcompat.widget.AppCompatEditText
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.runCatching import com.github.michaelbull.result.runCatching
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.msfjarvis.aps.R 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
import dev.msfjarvis.aps.util.pwgen.PasswordGenerator.generate import dev.msfjarvis.aps.util.pwgen.PasswordGenerator.generate
import dev.msfjarvis.aps.util.pwgen.PasswordGenerator.setPrefs import dev.msfjarvis.aps.util.pwgen.PasswordGenerator.setPrefs
@@ -30,41 +32,40 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys
class PasswordGeneratorDialogFragment : DialogFragment() { class PasswordGeneratorDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = MaterialAlertDialogBuilder(requireContext())
val callingActivity = requireActivity() val callingActivity = requireActivity()
val inflater = callingActivity.layoutInflater val binding = FragmentPwgenBinding.inflate(layoutInflater)
@SuppressLint("InflateParams")
val view = inflater.inflate(R.layout.fragment_pwgen, null)
val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf") val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf")
val prefs = requireActivity().applicationContext val prefs = requireActivity().applicationContext
.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) .getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
view.findViewById<CheckBox>(R.id.numerals)?.isChecked = !prefs.getBoolean(PasswordOption.NoDigits.key, false) builder.setView(binding.root)
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)
val textView: AppCompatEditText = view.findViewById(R.id.lengthNumber) binding.numerals.isChecked = !prefs.getBoolean(PasswordOption.NoDigits.key, false)
textView.setText(prefs.getInt(PreferenceKeys.LENGTH, 20).toString()) binding.symbols.isChecked = prefs.getBoolean(PasswordOption.AtLeastOneSymbol.key, false)
val passwordText: AppCompatTextView = view.findViewById(R.id.passwordText) binding.uppercase.isChecked = !prefs.getBoolean(PasswordOption.NoUppercaseLetters.key, false)
passwordText.typeface = monoTypeface binding.lowercase.isChecked = !prefs.getBoolean(PasswordOption.NoLowercaseLetters.key, false)
return MaterialAlertDialogBuilder(requireContext()).run { 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) setTitle(R.string.pwgen_title)
setView(view)
setPositiveButton(R.string.dialog_ok) { _, _ -> setPositiveButton(R.string.dialog_ok) { _, _ ->
val edit = callingActivity.findViewById<EditText>(R.id.password) setFragmentResult(
edit.setText(passwordText.text) PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY,
bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}")
)
} }
setNeutralButton(R.string.dialog_cancel) { _, _ -> } setNeutralButton(R.string.dialog_cancel) { _, _ -> }
setNegativeButton(R.string.pwgen_generate, null) setNegativeButton(R.string.pwgen_generate, null)
create() create()
}.apply { }.apply {
setOnShowListener { setOnShowListener {
generate(passwordText) generate(binding.passwordText)
getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
generate(passwordText) generate(binding.passwordText)
} }
} }
} }

View File

@@ -9,12 +9,12 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle import android.os.Bundle
import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import com.github.ajalt.timberkt.Timber.tag import com.github.ajalt.timberkt.Timber.tag
import com.github.michaelbull.result.fold import com.github.michaelbull.result.fold
import com.github.michaelbull.result.getOr import com.github.michaelbull.result.getOr
@@ -22,6 +22,7 @@ import com.github.michaelbull.result.runCatching
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.msfjarvis.aps.R import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.databinding.FragmentXkpwgenBinding 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.extensions.getString
import dev.msfjarvis.aps.util.pwgenxkpwd.CapsType import dev.msfjarvis.aps.util.pwgenxkpwd.CapsType
import dev.msfjarvis.aps.util.pwgenxkpwd.PasswordBuilder 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. */ /** A placeholder fragment containing a simple view. */
class XkPasswordGeneratorDialogFragment : DialogFragment() { class XkPasswordGeneratorDialogFragment : DialogFragment() {
private lateinit var prefs: SharedPreferences
private lateinit var binding: FragmentXkpwgenBinding
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = MaterialAlertDialogBuilder(requireContext()) val builder = MaterialAlertDialogBuilder(requireContext())
val callingActivity = requireActivity() val callingActivity = requireActivity()
val inflater = callingActivity.layoutInflater val inflater = callingActivity.layoutInflater
binding = FragmentXkpwgenBinding.inflate(inflater) val binding = FragmentXkpwgenBinding.inflate(inflater)
val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf") val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf")
val prefs = callingActivity.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
builder.setView(binding.root) builder.setView(binding.root)
prefs = callingActivity.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
val previousStoredCapStyle: String = runCatching { val previousStoredCapStyle: String = runCatching {
prefs.getString(PREF_KEY_CAPITALS_STYLE)!! prefs.getString(PREF_KEY_CAPITALS_STYLE)!!
}.getOr(DEFAULT_CAPS_STYLE) }.getOr(DEFAULT_CAPS_STYLE)
@@ -60,9 +56,11 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
binding.xkPasswordText.typeface = monoTypeface binding.xkPasswordText.typeface = monoTypeface
builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ ->
setPreferences() setPreferences(binding, prefs)
val edit = callingActivity.findViewById<EditText>(R.id.password) setFragmentResult(
edit.setText(binding.xkPasswordText.text) PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY,
bundleOf(PasswordCreationActivity.RESULT to "${binding.xkPasswordText.text}")
)
} }
// flip neutral and negative buttons // flip neutral and negative buttons
@@ -72,18 +70,18 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
val dialog = builder.setTitle(this.resources.getString(R.string.xkpwgen_title)).create() val dialog = builder.setTitle(this.resources.getString(R.string.xkpwgen_title)).create()
dialog.setOnShowListener { dialog.setOnShowListener {
setPreferences() setPreferences(binding, prefs)
makeAndSetPassword(binding.xkPasswordText) makeAndSetPassword(binding)
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
setPreferences() setPreferences(binding, prefs)
makeAndSetPassword(binding.xkPasswordText) makeAndSetPassword(binding)
} }
} }
return dialog return dialog
} }
private fun makeAndSetPassword(passwordText: AppCompatTextView) { private fun makeAndSetPassword(binding: FragmentXkpwgenBinding) {
PasswordBuilder(requireContext()) PasswordBuilder(requireContext())
.setNumberOfWords(Integer.valueOf(binding.xkNumWords.text.toString())) .setNumberOfWords(Integer.valueOf(binding.xkNumWords.text.toString()))
.setMinimumWordLength(DEFAULT_MIN_WORD_LENGTH) .setMinimumWordLength(DEFAULT_MIN_WORD_LENGTH)
@@ -93,16 +91,16 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
.appendSymbols(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_SYMBOL }) .appendSymbols(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_SYMBOL })
.setCapitalization(CapsType.valueOf(binding.xkCapType.selectedItem.toString())).create() .setCapitalization(CapsType.valueOf(binding.xkCapType.selectedItem.toString())).create()
.fold( .fold(
success = { passwordText.text = it }, success = { binding.xkPasswordText.text = it },
failure = { e -> failure = { e ->
Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show() Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show()
tag("xkpw").e(e, "failure generating xkpasswd") 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 { prefs.edit {
putString(PREF_KEY_CAPITALS_STYLE, binding.xkCapType.selectedItem.toString()) putString(PREF_KEY_CAPITALS_STYLE, binding.xkCapType.selectedItem.toString())
putString(PREF_KEY_NUM_WORDS, binding.xkNumWords.text.toString()) putString(PREF_KEY_NUM_WORDS, binding.xkNumWords.text.toString())

View File

@@ -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>

View File

@@ -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="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">Clear saved host key</string>
<string name="clear_saved_host_key_success">Successfully cleared 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> </resources>