diff --git a/app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt b/app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt index b0cde380..2923b117 100644 --- a/app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt +++ b/app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt @@ -42,7 +42,7 @@ constructor( ) { val keys = pgpKeyManager.getAllKeys().unwrap() // Iterates through the keys until the first successful decryption, then returns. - keys.first { key -> + keys.firstOrNull { key -> runCatching { pgpCryptoHandler.decrypt(key, password, message, out) }.isOk() } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt index 33ecdc2b..5b84478e 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt @@ -10,6 +10,7 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.lifecycle.lifecycleScope +import com.github.michaelbull.result.runCatching import dagger.hilt.android.AndroidEntryPoint import dev.msfjarvis.aps.R import dev.msfjarvis.aps.data.crypto.CryptoRepository @@ -17,6 +18,7 @@ import dev.msfjarvis.aps.data.passfile.PasswordEntry import dev.msfjarvis.aps.data.password.FieldItem import dev.msfjarvis.aps.databinding.DecryptLayoutBinding import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter +import dev.msfjarvis.aps.util.extensions.isErr import dev.msfjarvis.aps.util.extensions.unsafeLazy import dev.msfjarvis.aps.util.extensions.viewBinding import dev.msfjarvis.aps.util.settings.PreferenceKeys @@ -42,6 +44,7 @@ class DecryptActivityV2 : BasePgpActivity() { private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) } private var passwordEntry: PasswordEntry? = null + private var retries = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -56,7 +59,7 @@ class DecryptActivityV2 : BasePgpActivity() { true } } - decrypt() + decrypt(isError = false) } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -126,36 +129,51 @@ class DecryptActivityV2 : BasePgpActivity() { ) } - private fun decrypt() { + private fun decrypt(isError: Boolean) { + if (retries < MAX_RETRIES) { + retries += 1 + } else { + finish() + } val dialog = PasswordDialog() + if (isError) { + dialog.setError() + } lifecycleScope.launch(Dispatchers.Main) { dialog.password.collectLatest { value -> if (value != null) { - decrypt(value) + if (runCatching { decrypt(value) }.isErr()) { + decrypt(isError = true) + } } } } dialog.show(supportFragmentManager, "PASSWORD_DIALOG") } - private fun decrypt(password: String) { - lifecycleScope.launch { - val message = withContext(Dispatchers.IO) { File(fullPath).readBytes().inputStream() } - val result = - withContext(Dispatchers.IO) { - val outputStream = ByteArrayOutputStream() - repository.decrypt( - password, - message, - outputStream, - ) - outputStream - } - startAutoDismissTimer() + private suspend fun decrypt(password: String) { + val message = withContext(Dispatchers.IO) { File(fullPath).readBytes().inputStream() } + val result = + withContext(Dispatchers.IO) { + val outputStream = ByteArrayOutputStream() + repository.decrypt( + password, + message, + outputStream, + ) + outputStream + } + require(result.size() != 0) { "Incorrect password" } + startAutoDismissTimer() + val entry = passwordEntryFactory.create(lifecycleScope, result.toByteArray()) + passwordEntry = entry + createPasswordUi(entry) + } + + private suspend fun createPasswordUi(entry: PasswordEntry) = + withContext(Dispatchers.Main) { val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true) - val entry = passwordEntryFactory.create(lifecycleScope, result.toByteArray()) - passwordEntry = entry invalidateOptionsMenu() val items = arrayListOf() @@ -187,5 +205,8 @@ class DecryptActivityV2 : BasePgpActivity() { } } } + + private companion object { + private const val MAX_RETRIES = 3 } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt index 3542422a..d69a4686 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt @@ -8,6 +8,9 @@ package dev.msfjarvis.aps.ui.crypto import android.app.Dialog import android.content.DialogInterface import android.os.Bundle +import android.view.KeyEvent +import android.view.WindowManager +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import dev.msfjarvis.aps.R @@ -21,6 +24,7 @@ import kotlinx.coroutines.flow.asStateFlow class PasswordDialog : DialogFragment() { private val binding by unsafeLazy { DialogPasswordEntryBinding.inflate(layoutInflater) } + private var isError: Boolean = false private val _password = MutableStateFlow(null) val password = _password.asStateFlow() @@ -28,15 +32,37 @@ class PasswordDialog : DialogFragment() { val builder = MaterialAlertDialogBuilder(requireContext()) builder.setView(binding.root) builder.setTitle(R.string.password) - builder.setPositiveButton(android.R.string.ok) { _, _ -> - do {} while (!_password.tryEmit(binding.passwordEditText.text.toString())) - dismiss() + builder.setPositiveButton(android.R.string.ok) { _, _ -> tryEmitPassword() } + val dialog = builder.create() + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + dialog.setOnShowListener { + if (isError) { + binding.passwordField.error = getString(R.string.git_operation_wrong_password) + } + binding.passwordEditText.doOnTextChanged { _, _, _, _ -> binding.passwordField.error = null } + binding.passwordEditText.setOnKeyListener { _, keyCode, _ -> + if (keyCode == KeyEvent.KEYCODE_ENTER) { + tryEmitPassword() + return@setOnKeyListener true + } + false + } } - return builder.create() + return dialog + } + + fun setError() { + isError = true } override fun onCancel(dialog: DialogInterface) { super.onCancel(dialog) finish() } + + @Suppress("ControlFlowWithEmptyBody") + private fun tryEmitPassword() { + do {} while (!_password.tryEmit(binding.passwordEditText.text.toString())) + dismissAllowingStateLoss() + } } diff --git a/app/src/main/res/layout/dialog_password_entry.xml b/app/src/main/res/layout/dialog_password_entry.xml index 3729fb4f..0072d491 100644 --- a/app/src/main/res/layout/dialog_password_entry.xml +++ b/app/src/main/res/layout/dialog_password_entry.xml @@ -24,6 +24,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textPassword" /> +