Implement manual TOTP import and cleanup password generators (#1320)
This commit is contained in:
parent
051d455c9f
commit
92ece7dbb5
7 changed files with 171 additions and 48 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
46
app/src/main/res/layout/fragment_manual_otp_entry.xml
Normal file
46
app/src/main/res/layout/fragment_manual_otp_entry.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue