diff options
| author | Harsh Shandilya <msfjarvis@gmail.com> | 2020-08-05 19:02:24 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-08-05 19:02:24 +0530 |
| commit | 14c44bf584cb7d13ada46b264419efca923ed65c (patch) | |
| tree | b22c8a9a7be7934927ed20fa9b2060c877953082 /app/src/main/java/com/zeapo/pwdstore/git/operation | |
| parent | 12a83e5c36b82e42dda6a58248e2879fde9c3ac8 (diff) | |
| download | HardenedPass-14c44bf584cb7d13ada46b264419efca923ed65c.tar.gz HardenedPass-14c44bf584cb7d13ada46b264419efca923ed65c.zip | |
Remove GitAsyncTask and replace with non-blocking coroutines (#865)
Co-authored-by: Fabian Henneke <fabian@henneke.me>
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git/operation')
8 files changed, 479 insertions, 0 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt new file mode 100644 index 00000000..b46b66cf --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt @@ -0,0 +1,52 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git.operation + +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.git.GitCommandExecutor +import java.io.File +import org.eclipse.jgit.api.RebaseCommand + +class BreakOutOfDetached(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { + + private val branchName = "conflicting-$remoteBranch-${System.currentTimeMillis()}" + + override val commands = arrayOf( + // abort the rebase + git.rebase().setOperation(RebaseCommand.Operation.ABORT), + // git checkout -b conflict-branch + git.checkout().setCreateBranch(true).setName(branchName), + // push the changes + git.push().setRemote("origin"), + // switch back to ${gitBranch} + git.checkout().setName(remoteBranch), + ) + + override suspend fun execute() { + if (!git.repository.repositoryState.isRebasing) { + MaterialAlertDialogBuilder(callingActivity) + .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title)) + .setMessage("The repository is not rebasing, no need to push to another branch") + .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> + callingActivity.finish() + }.show() + return + } + GitCommandExecutor(callingActivity, this).execute() + } + + override fun onSuccess() { + MaterialAlertDialogBuilder(callingActivity) + .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title)) + .setMessage("There was a conflict when trying to rebase. " + + "Your local $remoteBranch branch was pushed to another branch named conflicting-$remoteBranch-....\n" + + "Use this branch to resolve conflict on your computer") + .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> + callingActivity.finish() + }.show() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt new file mode 100644 index 00000000..8c516eac --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt @@ -0,0 +1,29 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git.operation + +import androidx.appcompat.app.AppCompatActivity +import com.zeapo.pwdstore.git.GitCommandExecutor +import java.io.File +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.GitCommand + +/** + * Creates a new clone operation + * + * @param fileDir the git working tree directory + * @param uri URL to clone the repository from + * @param callingActivity the calling activity + */ +class CloneOperation(fileDir: File, uri: String, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { + + override val commands: Array<GitCommand<out Any>> = arrayOf( + Git.cloneRepository().setBranch(remoteBranch).setDirectory(repository?.workTree).setURI(uri), + ) + + override suspend fun execute() { + GitCommandExecutor(callingActivity, this).execute() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt new file mode 100644 index 00000000..423ceb80 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt @@ -0,0 +1,94 @@ +package com.zeapo.pwdstore.git.operation + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import androidx.annotation.StringRes +import androidx.core.content.edit +import androidx.fragment.app.FragmentActivity +import com.google.android.material.checkbox.MaterialCheckBox +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.git.config.ConnectionMode +import com.zeapo.pwdstore.git.config.InteractivePasswordFinder +import com.zeapo.pwdstore.utils.PreferenceKeys +import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.requestInputFocusOnView +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume + +class CredentialFinder( + val callingActivity: FragmentActivity, + val connectionMode: ConnectionMode +) : InteractivePasswordFinder() { + + override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) { + val gitOperationPrefs = callingActivity.getEncryptedPrefs("git_operation") + val credentialPref: String + @StringRes val messageRes: Int + @StringRes val hintRes: Int + @StringRes val rememberRes: Int + @StringRes val errorRes: Int + when (connectionMode) { + ConnectionMode.SshKey -> { + credentialPref = PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE + messageRes = R.string.passphrase_dialog_text + hintRes = R.string.ssh_keygen_passphrase + rememberRes = R.string.git_operation_remember_passphrase + errorRes = R.string.git_operation_wrong_passphrase + } + ConnectionMode.Password -> { + // Could be either an SSH or an HTTPS password + credentialPref = PreferenceKeys.HTTPS_PASSWORD + messageRes = R.string.password_dialog_text + hintRes = R.string.git_operation_hint_password + rememberRes = R.string.git_operation_remember_password + errorRes = R.string.git_operation_wrong_password + } + else -> throw IllegalStateException("Only SshKey and Password connection mode ask for passwords") + } + val storedCredential = gitOperationPrefs.getString(credentialPref, null) + if (isRetry) + gitOperationPrefs.edit { remove(credentialPref) } + if (storedCredential == null) { + val layoutInflater = LayoutInflater.from(callingActivity) + + @SuppressLint("InflateParams") + val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null) + val credentialLayout = dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout) + val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential) + editCredential.setHint(hintRes) + val rememberCredential = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential) + rememberCredential.setText(rememberRes) + if (isRetry) + credentialLayout.error = callingActivity.resources.getString(errorRes) + MaterialAlertDialogBuilder(callingActivity).run { + setTitle(R.string.passphrase_dialog_title) + setMessage(messageRes) + setView(dialogView) + setPositiveButton(R.string.dialog_ok) { _, _ -> + val credential = editCredential.text.toString() + if (rememberCredential.isChecked) { + gitOperationPrefs.edit { + putString(credentialPref, credential) + } + } + cont.resume(credential) + } + setNegativeButton(R.string.dialog_cancel) { _, _ -> + cont.resume(null) + } + setOnCancelListener { + cont.resume(null) + } + create() + }.run { + requestInputFocusOnView<TextInputEditText>(R.id.git_auth_credential) + show() + } + } else { + cont.resume(storedCredential) + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt new file mode 100644 index 00000000..45fefd64 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt @@ -0,0 +1,187 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git.operation + +import android.content.Intent +import androidx.annotation.CallSuper +import androidx.core.content.edit +import androidx.fragment.app.FragmentActivity +import androidx.preference.PreferenceManager +import com.github.ajalt.timberkt.Timber.d +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.UserPreference +import com.zeapo.pwdstore.git.ErrorMessages +import com.zeapo.pwdstore.git.config.ConnectionMode +import com.zeapo.pwdstore.git.config.InteractivePasswordFinder +import com.zeapo.pwdstore.git.config.SshApiSessionFactory +import com.zeapo.pwdstore.git.config.SshAuthData +import com.zeapo.pwdstore.git.config.SshjSessionFactory +import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.PreferenceKeys +import com.zeapo.pwdstore.utils.getEncryptedPrefs +import java.io.File +import net.schmizz.sshj.userauth.password.PasswordFinder +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.GitCommand +import org.eclipse.jgit.api.TransportCommand +import org.eclipse.jgit.errors.UnsupportedCredentialItem +import org.eclipse.jgit.transport.CredentialItem +import org.eclipse.jgit.transport.CredentialsProvider +import org.eclipse.jgit.transport.SshSessionFactory +import org.eclipse.jgit.transport.URIish + +/** + * Creates a new git operation + * + * @param gitDir the git working tree directory + * @param callingActivity the calling activity + */ +abstract class GitOperation(gitDir: File, internal val callingActivity: FragmentActivity) { + + abstract val commands: Array<GitCommand<out Any>> + private var provider: CredentialsProvider? = null + private val sshKeyFile = callingActivity.filesDir.resolve(".ssh_key") + private val hostKeyFile = callingActivity.filesDir.resolve(".host_key") + protected val repository = PasswordRepository.getRepository(gitDir) + protected val git = Git(repository) + protected val remoteBranch = PreferenceManager + .getDefaultSharedPreferences(callingActivity.applicationContext) + .getString(PreferenceKeys.GIT_BRANCH_NAME, "master") + + private class PasswordFinderCredentialsProvider(private val username: String, private val passwordFinder: PasswordFinder) : CredentialsProvider() { + + override fun isInteractive() = true + + override fun get(uri: URIish?, vararg items: CredentialItem): Boolean { + for (item in items) { + when (item) { + is CredentialItem.Username -> item.value = username + is CredentialItem.Password -> item.value = passwordFinder.reqPassword(null) + else -> UnsupportedCredentialItem(uri, item.javaClass.name) + } + } + return true + } + + override fun supports(vararg items: CredentialItem) = items.all { + it is CredentialItem.Username || it is CredentialItem.Password + } + } + + private fun withPasswordAuthentication(username: String, passwordFinder: InteractivePasswordFinder): GitOperation { + val sessionFactory = SshjSessionFactory(username, SshAuthData.Password(passwordFinder), hostKeyFile) + SshSessionFactory.setInstance(sessionFactory) + this.provider = PasswordFinderCredentialsProvider(username, passwordFinder) + return this + } + + private fun withPublicKeyAuthentication(username: String, passphraseFinder: InteractivePasswordFinder): GitOperation { + val sessionFactory = SshjSessionFactory(username, SshAuthData.PublicKeyFile(sshKeyFile, passphraseFinder), hostKeyFile) + SshSessionFactory.setInstance(sessionFactory) + this.provider = null + return this + } + + private fun withOpenKeychainAuthentication(username: String, identity: SshApiSessionFactory.ApiIdentity?): GitOperation { + SshSessionFactory.setInstance(SshApiSessionFactory(username, identity)) + this.provider = null + return this + } + + private fun getSshKey(make: Boolean) { + try { + // Ask the UserPreference to provide us with the ssh-key + // onResult has to be handled by the callingActivity + val intent = Intent(callingActivity.applicationContext, UserPreference::class.java) + intent.putExtra("operation", if (make) "make_ssh_key" else "get_ssh_key") + callingActivity.startActivityForResult(intent, GET_SSH_KEY_FROM_CLONE) + } catch (e: Exception) { + println("Exception caught :(") + e.printStackTrace() + } + } + + fun setCredentialProvider() { + provider?.let { credentialsProvider -> + commands.filterIsInstance<TransportCommand<*, *>>().forEach { it.setCredentialsProvider(credentialsProvider) } + } + } + + /** + * Executes the GitCommand in an async task + */ + abstract suspend fun execute() + + suspend fun executeAfterAuthentication( + connectionMode: ConnectionMode, + username: String, + identity: SshApiSessionFactory.ApiIdentity? + ) { + when (connectionMode) { + ConnectionMode.SshKey -> if (!sshKeyFile.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)) { _, _ -> + getSshKey(false) + } + .setNegativeButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)) { _, _ -> + getSshKey(true) + } + .setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> + // Finish the blank GitActivity so user doesn't have to press back + callingActivity.finish() + }.show() + } else { + withPublicKeyAuthentication(username, CredentialFinder(callingActivity, + connectionMode)).execute() + } + ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(username, identity).execute() + ConnectionMode.Password -> withPasswordAuthentication( + username, CredentialFinder(callingActivity, connectionMode)).execute() + ConnectionMode.None -> execute() + } + } + + /** + * Action to execute on error + */ + @CallSuper + open fun onError(err: Exception) { + // Clear various auth related fields on failure + when (SshSessionFactory.getInstance()) { + is SshApiSessionFactory -> { + PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext) + .edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) } + } + is SshjSessionFactory -> { + callingActivity.applicationContext + .getEncryptedPrefs("git_operation") + .edit { + remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) + remove(PreferenceKeys.HTTPS_PASSWORD) + } + } + } + d(err) + MaterialAlertDialogBuilder(callingActivity) + .setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title)) + .setMessage(ErrorMessages[err]) + .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> + callingActivity.finish() + }.show() + } + + /** + * Action to execute on success + */ + open fun onSuccess() {} + + companion object { + + const val GET_SSH_KEY_FROM_CLONE = 201 + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt new file mode 100644 index 00000000..d31b2aa4 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git.operation + +import androidx.appcompat.app.AppCompatActivity +import com.zeapo.pwdstore.git.GitCommandExecutor +import java.io.File +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.GitCommand + +/** + * Creates a new git operation + * + * @param fileDir the git working tree directory + * @param callingActivity the calling activity + */ +class PullOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { + + override val commands: Array<GitCommand<out Any>> = arrayOf( + Git(repository).pull().setRebase(true).setRemote("origin"), + ) + + override suspend fun execute() { + GitCommandExecutor(callingActivity, this).execute() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt new file mode 100644 index 00000000..33b20a06 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt @@ -0,0 +1,29 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git.operation + +import androidx.appcompat.app.AppCompatActivity +import com.zeapo.pwdstore.git.GitCommandExecutor +import java.io.File +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.GitCommand + +/** + * Creates a new git operation + * + * @param fileDir the git working tree directory + * @param callingActivity the calling activity + */ +class PushOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { + + override val commands: Array<GitCommand<out Any>> = arrayOf( + Git(repository).push().setPushAll().setRemote("origin"), + ) + + override suspend fun execute() { + setCredentialProvider() + GitCommandExecutor(callingActivity, this).execute() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt new file mode 100644 index 00000000..f0aee212 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt @@ -0,0 +1,30 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git.operation + +import androidx.appcompat.app.AppCompatActivity +import com.zeapo.pwdstore.git.GitCommandExecutor +import java.io.File +import org.eclipse.jgit.api.ResetCommand + +/** + * Creates a new git operation + * + * @param fileDir the git working tree directory + * @param callingActivity the calling activity + */ +class ResetToRemoteOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { + + override val commands = arrayOf( + git.add().addFilepattern("."), + git.fetch().setRemote("origin"), + git.reset().setRef("origin/$remoteBranch").setMode(ResetCommand.ResetType.HARD), + git.branchCreate().setName(remoteBranch).setForce(true), + ) + + override suspend fun execute() { + GitCommandExecutor(callingActivity, this).execute() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt new file mode 100644 index 00000000..6edf2994 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt @@ -0,0 +1,30 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git.operation + +import androidx.appcompat.app.AppCompatActivity +import com.zeapo.pwdstore.git.GitCommandExecutor +import java.io.File + +/** + * Creates a new git operation + * + * @param fileDir the git working tree directory + * @param callingActivity the calling activity + */ +class SyncOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { + + override val commands = arrayOf( + git.add().addFilepattern("."), + git.status(), + git.commit().setAll(true).setMessage("[Android Password Store] Sync"), + git.pull().setRebase(true).setRemote("origin"), + git.push().setPushAll().setRemote("origin"), + ) + + override suspend fun execute() { + GitCommandExecutor(callingActivity, this).execute() + } +} |
