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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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.
- 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

View file

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

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

View file

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

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="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>