From b94b52a42ddc2325b539d0956fd0adcf35308b52 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 17 Apr 2020 18:36:07 +0530 Subject: Refactor Git related activities (#685) * Refactor git logic into separate parts * Extract hardcoded strings * Add KDoc to updateHostname, remove unused field * Cleanups * Fix dialog message * Wire in repository clone flow * spotless * Remove unused method * Cleanup GitActivity - Rename to GitOperationActivity. - Ensure identityBuilder is always closed regardless of what fragment uses it. - Remove hardcoded "Operation" strings and replace with REQUEST_ARG_OP. - Apply a transparent theme to GitOperationActivity make the UI less jarring. * Tweak some stupidly worded dialog messages As pointed out in #629, these strings are shoddily worded and do not express any clear intent to the user, leaving them confused and angry. * GitOperationActivity: wrap Context to ensure right theme is used * spotless * undo build.gradle change * Use correct parent theme, remove now useless wrapping * GitServerConfigActivity: fix repository clone flow * temp: disable leakcanary framework leaks on Samsung are pissing me off * Make system bars transparent in git activity * Tweak HTTPS password layout * Unhardcode wrong passphrase string * Store SSH passphrase in EncryptedSharedPreferences Also revamp the dialog to look a bit better * Implement support for remembering HTTPS password Fixes #521 * Try to patch HTTPS remote creation logic * Update security-crypto * Clear saved passphrase/password on auth failure * Revert "Update security-crypto" Broken on R DP2.1 This reverts commit 4b20371dd42c512a3dd3b759859abb6c1ffd2961. * Revert "temp: disable leakcanary" This reverts commit 2db7d41bd67b79c6dc8c5b359a7b27100379f45f. * Update CHANGELOG * Remove spacer * Remove useless override * Wrap git server activity in a ScrollView * GitOperation: always finish calling activity when dialogs are dismissed * Wipe saved password/passphrase when hostname changes * Don't commit prefs updates * Don't call listFiles excessively * Finish activity after saving configuration * Make ConnectionMode and Protocol enum classes * Change SSH key passphrase key, don't wipe on host change * Reimplement BaseGitActivity.updateUrl (was updateHostname) * Use SharedPreferences.edit KTX extension * Disable inapplicable connection modes depending on scheme * BaseGitActivity: annotate onDestroy with CallSuper We'll leak the identityBuilder connection otherwise * Move input hack for AlertDialog into an extension function We re-use this in many places * Fix protocol/mode toggle issue and consistenly name options * Fix a crash when opening GitServerConfigActivity without a repo * Fix OpenKeychain callbacks by moving onActivityResult to BaseGitActivity * Run spotlessApply Signed-off-by: Harsh Shandilya Co-authored-by: Fabian Henneke --- .../java/com/zeapo/pwdstore/git/GitOperation.kt | 166 ++++++++++++--------- 1 file changed, 97 insertions(+), 69 deletions(-) (limited to 'app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt') diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt index ea9ee654..78a9ca69 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt @@ -7,23 +7,24 @@ package com.zeapo.pwdstore.git import android.annotation.SuppressLint import android.app.Activity import android.content.Intent -import android.text.InputType import android.view.LayoutInflater -import android.view.View -import android.widget.CheckBox -import android.widget.EditText -import android.widget.LinearLayout +import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText import com.jcraft.jsch.JSch import com.jcraft.jsch.JSchException import com.jcraft.jsch.KeyPair import com.zeapo.pwdstore.R import com.zeapo.pwdstore.UserPreference +import com.zeapo.pwdstore.git.config.ConnectionMode import com.zeapo.pwdstore.git.config.GitConfigSessionFactory import com.zeapo.pwdstore.git.config.SshApiSessionFactory import com.zeapo.pwdstore.git.config.SshConfigSessionFactory import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.requestInputFocusOnView import java.io.File import org.eclipse.jgit.api.GitCommand import org.eclipse.jgit.lib.Repository @@ -96,7 +97,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit * @param identity the api identity to use for auth in OpenKeychain connection mode */ fun executeAfterAuthentication( - connectionMode: String, + connectionMode: ConnectionMode, username: String, sshKey: File?, identity: SshApiSessionFactory.ApiIdentity? @@ -114,15 +115,17 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit * @param showError show the passphrase edit text in red */ private fun executeAfterAuthentication( - connectionMode: String, + connectionMode: ConnectionMode, username: String, sshKey: File?, identity: SshApiSessionFactory.ApiIdentity?, showError: Boolean ) { - if (connectionMode.equals("ssh-key", ignoreCase = true)) { - if (sshKey == null || !sshKey.exists()) { - MaterialAlertDialogBuilder(callingActivity) + val encryptedSettings = callingActivity.applicationContext.getEncryptedPrefs("git_operation") + when (connectionMode) { + ConnectionMode.SshKey -> { + if (sshKey == null || !sshKey.exists()) { + MaterialAlertDialogBuilder(callingActivity) .setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text)) .setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title)) .setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ -> @@ -152,82 +155,100 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit // Finish the blank GitActivity so user doesn't have to press back callingActivity.finish() }.show() - } else { - val layoutInflater = LayoutInflater.from(callingActivity.applicationContext) - @SuppressLint("InflateParams") val dialogView = layoutInflater.inflate(R.layout.git_passphrase_layout, null) - val passphrase = dialogView.findViewById(R.id.sshkey_passphrase) - val settings = PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext) - val sshKeyPassphrase = settings.getString("ssh_key_passphrase", null) - if (showError) { - passphrase.error = "Wrong passphrase" - } - val jsch = JSch() - try { - val keyPair = KeyPair.load(jsch, callingActivity.filesDir.toString() + "/.ssh_key") + } else { + val layoutInflater = LayoutInflater.from(callingActivity) + @SuppressLint("InflateParams") val dialogView = layoutInflater.inflate(R.layout.git_passphrase_layout, null) + val passphrase = dialogView.findViewById(R.id.git_auth_passphrase) + val sshKeyPassphrase = encryptedSettings.getString("ssh_key_local_passphrase", null) + if (showError) { + passphrase.error = callingActivity.resources.getString(R.string.git_operation_wrong_passphrase) + } + val jsch = JSch() + try { + val keyPair = KeyPair.load(jsch, callingActivity.filesDir.toString() + "/.ssh_key") - if (keyPair.isEncrypted) { - if (sshKeyPassphrase != null && sshKeyPassphrase.isNotEmpty()) { - if (keyPair.decrypt(sshKeyPassphrase)) { - // Authenticate using the ssh-key and then execute the command - setAuthentication(sshKey, username, sshKeyPassphrase).execute() + if (keyPair.isEncrypted) { + if (sshKeyPassphrase != null && sshKeyPassphrase.isNotEmpty()) { + if (keyPair.decrypt(sshKeyPassphrase)) { + // Authenticate using the ssh-key and then execute the command + setAuthentication(sshKey, username, sshKeyPassphrase).execute() + } else { + // call back the method + executeAfterAuthentication(connectionMode, username, sshKey, identity, true) + } } else { - // call back the method - executeAfterAuthentication(connectionMode, username, sshKey, identity, true) - } - } else { - MaterialAlertDialogBuilder(callingActivity) + val dialog = MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title)) .setMessage(callingActivity.resources.getString(R.string.passphrase_dialog_text)) .setView(dialogView) .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> if (keyPair.decrypt(passphrase.text.toString())) { - val rememberPassphrase = (dialogView.findViewById(R.id.sshkey_remember_passphrase) as CheckBox).isChecked + val rememberPassphrase = dialogView.findViewById(R.id.git_auth_remember_passphrase).isChecked if (rememberPassphrase) { - settings.edit().putString("ssh_key_passphrase", passphrase.text.toString()).apply() + encryptedSettings.edit().putString("ssh_key_local_passphrase", passphrase.text.toString()).apply() } // Authenticate using the ssh-key and then execute the command setAuthentication(sshKey, username, passphrase.text.toString()).execute() } else { - settings.edit().putString("ssh_key_passphrase", null).apply() + encryptedSettings.edit().putString("ssh_key_local_passphrase", null).apply() // call back the method executeAfterAuthentication(connectionMode, username, sshKey, identity, true) } - }.setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> - // Do nothing. - }.show() + } + .setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> + callingActivity.finish() + } + .setOnCancelListener { callingActivity.finish() } + .create() + dialog.requestInputFocusOnView(R.id.git_auth_passphrase) + dialog.show() + } + } else { + setAuthentication(sshKey, username, "").execute() } - } else { - setAuthentication(sshKey, username, "").execute() - } - } catch (e: JSchException) { - e.printStackTrace() - MaterialAlertDialogBuilder(callingActivity) - .setTitle("Unable to open the ssh-key") - .setMessage("Please check that it was imported.") - .setPositiveButton("Ok") { _, _ -> callingActivity.finish() } + } catch (e: JSchException) { + e.printStackTrace() + MaterialAlertDialogBuilder(callingActivity) + .setTitle(callingActivity.resources.getString(R.string.git_operation_unable_to_open_ssh_key_title)) + .setMessage(callingActivity.resources.getString(R.string.git_operation_unable_to_open_ssh_key_message)) + .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> + callingActivity.finish() + } .show() + } + } + } + ConnectionMode.OpenKeychain -> { + setAuthentication(username, identity).execute() + } + ConnectionMode.Password -> { + @SuppressLint("InflateParams") val dialogView = callingActivity.layoutInflater.inflate(R.layout.git_passphrase_layout, null) + val passwordView = dialogView.findViewById(R.id.git_auth_passphrase) + val password = encryptedSettings.getString("https_password", null) + if (password != null && password.isNotEmpty()) { + setAuthentication(username, password).execute() + } else { + val dialog = MaterialAlertDialogBuilder(callingActivity) + .setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title)) + .setMessage(callingActivity.resources.getString(R.string.password_dialog_text)) + .setView(dialogView) + .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> + val rememberPassphrase = dialogView.findViewById(R.id.git_auth_remember_passphrase).isChecked + if (rememberPassphrase) { + encryptedSettings.edit().putString("https_password", passwordView.text.toString()).apply() + } + // authenticate using the user/pwd and then execute the command + setAuthentication(username, passwordView.text.toString()).execute() + } + .setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> + callingActivity.finish() + } + .setOnCancelListener { callingActivity.finish() } + .create() + dialog.requestInputFocusOnView(R.id.git_auth_passphrase) + dialog.show() } } - } else if (connectionMode.equals("OpenKeychain", ignoreCase = true)) { - setAuthentication(username, identity).execute() - } else { - val password = EditText(callingActivity) - password.hint = "Password" - password.width = LinearLayout.LayoutParams.MATCH_PARENT - password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - - MaterialAlertDialogBuilder(callingActivity) - .setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title)) - .setMessage(callingActivity.resources.getString(R.string.password_dialog_text)) - .setView(password) - .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> - // authenticate using the user/pwd and then execute the command - setAuthentication(username, password.text.toString()).execute() - } - .setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> - callingActivity.finish() - } - .show() } } @@ -235,10 +256,17 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit * Action to execute on error */ open fun onError(errorMessage: String) { + // Clear various auth related fields on failure if (SshSessionFactory.getInstance() is SshApiSessionFactory) { - // Clear stored key id from settings on auth failure PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext) - .edit().putString("ssh_openkeystore_keyid", null).apply() + .edit { putString("ssh_openkeystore_keyid", null) } + callingActivity.applicationContext + .getEncryptedPrefs("git_operation") + .edit { remove("ssh_key_local_passphrase") } + } else if (SshSessionFactory.getInstance() is GitConfigSessionFactory) { + callingActivity.applicationContext + .getEncryptedPrefs("git_operation") + .edit { remove("https_password") } } } -- cgit v1.2.3