aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/git/operation
diff options
context:
space:
mode:
authorHarsh Shandilya <msfjarvis@gmail.com>2020-08-05 19:02:24 +0530
committerGitHub <noreply@github.com>2020-08-05 19:02:24 +0530
commit14c44bf584cb7d13ada46b264419efca923ed65c (patch)
treeb22c8a9a7be7934927ed20fa9b2060c877953082 /app/src/main/java/com/zeapo/pwdstore/git/operation
parent12a83e5c36b82e42dda6a58248e2879fde9c3ac8 (diff)
downloadHardenedPass-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')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt52
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt29
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt94
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt187
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt28
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt29
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt30
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt30
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()
+ }
+}