Refactor Git operations and auth (#1066)

Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Fabian Henneke 2020-09-03 10:48:14 +02:00 committed by GitHub
parent 258ccc6016
commit 3840f43fa0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 276 additions and 493 deletions

View file

@ -118,6 +118,7 @@ dependencies {
implementation(Dependencies.ThirdParty.jgit) { implementation(Dependencies.ThirdParty.jgit) {
exclude(group = "org.apache.httpcomponents", module = "httpclient") exclude(group = "org.apache.httpcomponents", module = "httpclient")
} }
implementation(Dependencies.ThirdParty.kotlin_result)
implementation(Dependencies.ThirdParty.sshj) implementation(Dependencies.ThirdParty.sshj)
implementation(Dependencies.ThirdParty.bouncycastle) implementation(Dependencies.ThirdParty.bouncycastle)
implementation(Dependencies.ThirdParty.plumber) implementation(Dependencies.ThirdParty.plumber)

View file

@ -18,6 +18,7 @@
-keepattributes SourceFile,LineNumberTable -keepattributes SourceFile,LineNumberTable
-dontobfuscate -dontobfuscate
-keep class com.jcraft.jsch.**
-keep class org.eclipse.jgit.internal.JGitText { *; } -keep class org.eclipse.jgit.internal.JGitText { *; }
-keep class org.bouncycastle.jcajce.provider.** { *; } -keep class org.bouncycastle.jcajce.provider.** { *; }
-keep class org.bouncycastle.jce.provider.** { *; } -keep class org.bouncycastle.jce.provider.** { *; }

View file

@ -51,10 +51,6 @@
android:windowSoftInputMode="stateAlwaysHidden" android:windowSoftInputMode="stateAlwaysHidden"
tools:node="replace" /> tools:node="replace" />
<activity
android:name=".git.GitOperationActivity"
android:theme="@style/NoBackgroundTheme" />
<activity <activity
android:name=".git.GitServerConfigActivity" android:name=".git.GitServerConfigActivity"
android:label="@string/title_activity_git_clone" android:label="@string/title_activity_git_clone"

View file

@ -40,17 +40,17 @@ class LaunchActivity : AppCompatActivity() {
} }
private fun startTargetActivity(noAuth: Boolean) { private fun startTargetActivity(noAuth: Boolean) {
if (intent.action == ACTION_DECRYPT_PASS) { val intentToStart = if (intent.action == ACTION_DECRYPT_PASS)
Intent(this, DecryptActivity::class.java).apply { Intent(this, DecryptActivity::class.java).apply {
putExtra("NAME", intent.getStringExtra("NAME")) putExtra("NAME", intent.getStringExtra("NAME"))
putExtra("FILE_PATH", intent.getStringExtra("FILE_PATH")) putExtra("FILE_PATH", intent.getStringExtra("FILE_PATH"))
putExtra("REPO_PATH", intent.getStringExtra("REPO_PATH")) putExtra("REPO_PATH", intent.getStringExtra("REPO_PATH"))
putExtra("LAST_CHANGED_TIMESTAMP", intent.getLongExtra("LAST_CHANGED_TIMESTAMP", 0L)) putExtra("LAST_CHANGED_TIMESTAMP", intent.getLongExtra("LAST_CHANGED_TIMESTAMP", 0L))
startActivity(this)
}
} else {
startActivity(Intent(this, PasswordStore::class.java))
} }
else
Intent(this, PasswordStore::class.java)
startActivity(intentToStart)
Handler().postDelayed({ finish() }, if (noAuth) 0L else 500L) Handler().postDelayed({ finish() }, if (noAuth) 0L else 500L)
} }

View file

@ -19,12 +19,13 @@ import androidx.appcompat.view.ActionMode
import androidx.core.content.edit import androidx.core.content.edit
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.michaelbull.result.fold
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding
import com.zeapo.pwdstore.git.BaseGitActivity import com.zeapo.pwdstore.git.BaseGitActivity
import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.git.config.AuthMode import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings import com.zeapo.pwdstore.git.config.GitSettings
@ -39,6 +40,7 @@ import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.viewBinding import com.zeapo.pwdstore.utils.viewBinding
import java.io.File import java.io.File
import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder import me.zhanghai.android.fastscroll.FastScrollerBuilder
class PasswordFragment : Fragment(R.layout.password_recycler_view) { class PasswordFragment : Fragment(R.layout.password_recycler_view) {
@ -99,9 +101,17 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
AuthMode.None -> BaseGitActivity.REQUEST_PULL AuthMode.None -> BaseGitActivity.REQUEST_PULL
else -> BaseGitActivity.REQUEST_SYNC else -> BaseGitActivity.REQUEST_SYNC
} }
val intent = Intent(context, GitOperationActivity::class.java) requireStore().apply {
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, operationId) lifecycleScope.launch {
swipeResult.launch(intent) launchGitOperation(operationId).fold(
success = {
binding.swipeRefresher.isRefreshing = false
refreshPasswordList()
},
failure = ::finishAfterPromptOnErrorHandler,
)
}
}
} }
} }

View file

@ -23,7 +23,6 @@ import android.view.MenuItem.OnActionExpandListener
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener import androidx.appcompat.widget.SearchView.OnQueryTextListener
@ -38,6 +37,7 @@ import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.i import com.github.ajalt.timberkt.i
import com.github.ajalt.timberkt.w import com.github.ajalt.timberkt.w
import com.github.michaelbull.result.fold
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
@ -48,7 +48,6 @@ import com.zeapo.pwdstore.crypto.BasePgpActivity.Companion.getLongName
import com.zeapo.pwdstore.crypto.DecryptActivity import com.zeapo.pwdstore.crypto.DecryptActivity
import com.zeapo.pwdstore.crypto.PasswordCreationActivity import com.zeapo.pwdstore.crypto.PasswordCreationActivity
import com.zeapo.pwdstore.git.BaseGitActivity import com.zeapo.pwdstore.git.BaseGitActivity
import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.git.config.AuthMode import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings import com.zeapo.pwdstore.git.config.GitSettings
@ -81,7 +80,7 @@ import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.errors.GitAPIException import org.eclipse.jgit.api.errors.GitAPIException
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { class PasswordStore : BaseGitActivity() {
private lateinit var activity: PasswordStore private lateinit var activity: PasswordStore
private lateinit var searchItem: MenuItem private lateinit var searchItem: MenuItem
@ -101,12 +100,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
} }
} }
private val listRefreshAction = registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
refreshPasswordList()
}
}
private val repositoryInitAction = registerForActivityResult(StartActivityForResult()) { result -> private val repositoryInitAction = registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
initializeRepositoryInfo() initializeRepositoryInfo()
@ -136,7 +129,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
private val checkPermissionsAndCloneAction = registerForActivityResult(RequestPermission()) { granted -> private val checkPermissionsAndCloneAction = registerForActivityResult(RequestPermission()) { granted ->
if (granted) { if (granted) {
val intent = Intent(activity, GitServerConfigActivity::class.java) val intent = Intent(activity, GitServerConfigActivity::class.java)
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE) intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE)
cloneAction.launch(intent) cloneAction.launch(intent)
} }
} }
@ -163,10 +156,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
@SuppressLint("NewApi") @SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
activity = this activity = this
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
shortcutManager = getSystemService()
}
// If user opens app with permission granted then revokes and returns, // If user opens app with permission granted then revokes and returns,
// prevent attempt to create password list fragment // prevent attempt to create password list fragment
var savedInstance = savedInstanceState var savedInstance = savedInstanceState
@ -177,6 +166,10 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
savedInstance = null savedInstance = null
} }
super.onCreate(savedInstance) super.onCreate(savedInstance)
setContentView(R.layout.activity_pwdstore)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
shortcutManager = getSystemService()
}
// If user is eligible for Oreo autofill, prompt them to switch. // If user is eligible for Oreo autofill, prompt them to switch.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
@ -328,9 +321,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
initBefore.show() initBefore.show()
return false return false
} }
intent = Intent(this, GitOperationActivity::class.java) runGitOperation(REQUEST_PUSH)
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_PUSH)
startActivity(intent)
return true return true
} }
R.id.git_pull -> { R.id.git_pull -> {
@ -338,9 +329,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
initBefore.show() initBefore.show()
return false return false
} }
intent = Intent(this, GitOperationActivity::class.java) runGitOperation(REQUEST_PULL)
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_PULL)
listRefreshAction.launch(intent)
return true return true
} }
R.id.git_sync -> { R.id.git_sync -> {
@ -348,9 +337,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
initBefore.show() initBefore.show()
return false return false
} }
intent = Intent(this, GitOperationActivity::class.java) runGitOperation(REQUEST_SYNC)
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_SYNC)
listRefreshAction.launch(intent)
return true return true
} }
R.id.refresh -> { R.id.refresh -> {
@ -415,6 +402,13 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
createRepository() createRepository()
} }
private fun runGitOperation(operation: Int) = lifecycleScope.launch {
launchGitOperation(operation).fold(
success = { refreshPasswordList() },
failure = ::finishAfterPromptOnErrorHandler,
)
}
/** /**
* Validates if storage permission is granted, and requests for it if not. The return value * Validates if storage permission is granted, and requests for it if not. The return value
* is true if the permission has been granted. * is true if the permission has been granted.
@ -576,12 +570,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
intent.putExtra("REPO_PATH", getRepositoryDirectory().absolutePath) intent.putExtra("REPO_PATH", getRepositoryDirectory().absolutePath)
registerForActivityResult(StartActivityForResult()) { result -> registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
lifecycleScope.launch {
commitChange(
resources.getString(R.string.git_commit_add_text, result.data?.extras?.getString("LONG_NAME")),
finishActivityOnEnd = false,
)
}
refreshPasswordList() refreshPasswordList()
} }
}.launch(intent) }.launch(intent)
@ -624,7 +612,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
lifecycleScope.launch { lifecycleScope.launch {
commitChange( commitChange(
resources.getString(R.string.git_commit_remove_text, fmt), resources.getString(R.string.git_commit_remove_text, fmt),
finishActivityOnEnd = false,
) )
} }
} }
@ -636,7 +623,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
val intent = Intent(this, SelectFolderActivity::class.java) val intent = Intent(this, SelectFolderActivity::class.java)
val fileLocations = values.map { it.file.absolutePath }.toTypedArray() val fileLocations = values.map { it.file.absolutePath }.toTypedArray()
intent.putExtra("Files", fileLocations) intent.putExtra("Files", fileLocations)
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, "SELECTFOLDER") intent.putExtra(REQUEST_ARG_OP, "SELECTFOLDER")
registerForActivityResult(StartActivityForResult()) { result -> registerForActivityResult(StartActivityForResult()) { result ->
val intentData = result.data ?: return@registerForActivityResult val intentData = result.data ?: return@registerForActivityResult
val filesToMove = requireNotNull(intentData.getStringArrayExtra("Files")) val filesToMove = requireNotNull(intentData.getStringArrayExtra("Files"))
@ -694,7 +681,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
commitChange( commitChange(
resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName), resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName),
finishActivityOnEnd = false,
) )
} }
} }
@ -704,7 +690,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
commitChange( commitChange(
resources.getString(R.string.git_commit_move_multiple_text, relativePath), resources.getString(R.string.git_commit_move_multiple_text, relativePath),
finishActivityOnEnd = false,
) )
} }
} }
@ -769,7 +754,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
commitChange( commitChange(
resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name), resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name),
finishActivityOnEnd = false,
) )
} }
} }
@ -848,7 +832,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
NEW_REPO_BUTTON -> initializeRepositoryInfo() NEW_REPO_BUTTON -> initializeRepositoryInfo()
CLONE_REPO_BUTTON -> { CLONE_REPO_BUTTON -> {
val intent = Intent(activity, GitServerConfigActivity::class.java) val intent = Intent(activity, GitServerConfigActivity::class.java)
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE) intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE)
cloneAction.launch(intent) cloneAction.launch(intent)
} }
} }
@ -872,7 +856,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
NEW_REPO_BUTTON -> initializeRepositoryInfo() NEW_REPO_BUTTON -> initializeRepositoryInfo()
CLONE_REPO_BUTTON -> { CLONE_REPO_BUTTON -> {
val intent = Intent(activity, GitServerConfigActivity::class.java) val intent = Intent(activity, GitServerConfigActivity::class.java)
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE) intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE)
cloneAction.launch(intent) cloneAction.launch(intent)
} }
} }

View file

@ -15,9 +15,7 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.autofill.oreo.AutofillAction import com.zeapo.pwdstore.autofill.oreo.AutofillAction
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
@ -26,9 +24,7 @@ import com.zeapo.pwdstore.autofill.oreo.FillableForm
import com.zeapo.pwdstore.autofill.oreo.FormOrigin import com.zeapo.pwdstore.autofill.oreo.FormOrigin
import com.zeapo.pwdstore.crypto.PasswordCreationActivity import com.zeapo.pwdstore.crypto.PasswordCreationActivity
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.commitChange
import java.io.File import java.io.File
import kotlinx.coroutines.launch
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
class AutofillSaveActivity : AppCompatActivity() { class AutofillSaveActivity : AppCompatActivity() {
@ -119,7 +115,6 @@ class AutofillSaveActivity : AppCompatActivity() {
formOrigin?.let { formOrigin?.let {
AutofillMatcher.addMatchFor(this, it, File(createdPath)) AutofillMatcher.addMatchFor(this, it, File(createdPath))
} }
val longName = data.getStringExtra("LONG_NAME")!!
val password = data.getStringExtra("PASSWORD") val password = data.getStringExtra("PASSWORD")
val resultIntent = if (password != null) { val resultIntent = if (password != null) {
// Password was generated and should be filled into a form. // Password was generated and should be filled into a form.
@ -144,15 +139,8 @@ class AutofillSaveActivity : AppCompatActivity() {
// Password was extracted from a form, there is nothing to fill. // Password was extracted from a form, there is nothing to fill.
Intent() Intent()
} }
// PasswordCreationActivity delegates committing the added file to PasswordStore. Since setResult(RESULT_OK, resultIntent)
// PasswordStore is not involved in an AutofillScenario, we have to commit the file ourselves. finish()
lifecycleScope.launch {
commitChange(
getString(R.string.git_commit_add_text, longName),
finishWithResultOnEnd = resultIntent
)
}
// GitAsyncTask will finish the activity for us.
} }
}.launch(saveIntent) }.launch(saveIntent)
} }

View file

@ -20,6 +20,7 @@ import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.github.michaelbull.result.onSuccess
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
import com.google.zxing.integration.android.IntentIntegrator.QR_CODE import com.google.zxing.integration.android.IntentIntegrator.QR_CODE
@ -331,16 +332,15 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds ->
gpgIdentifierFile.writeText(keyIds.joinToString("\n")) gpgIdentifierFile.writeText(keyIds.joinToString("\n"))
lifecycleScope.launch { lifecycleScope.launch {
commitChange( commitChange(getString(
getString(
R.string.git_commit_gpg_id, R.string.git_commit_gpg_id,
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
) )).onSuccess {
)
}
encrypt(data) encrypt(data)
} }
} }
}
}
}.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)) }.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java))
return@with return@with
} }
@ -440,17 +440,6 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
returnIntent.putExtra(RETURN_EXTRA_USERNAME, username) returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
} }
if (editing) {
lifecycleScope.launch {
commitChange(
getString(
R.string.git_commit_edit_text,
getLongName(fullPath, repoPath, editName)
)
)
}
}
if (directoryInputLayout.isVisible && directoryInputLayout.isEnabled && oldFileName != null) { if (directoryInputLayout.isVisible && directoryInputLayout.isEnabled && oldFileName != null) {
val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg") val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg")
if (oldFile.path != file.path && !oldFile.delete()) { if (oldFile.path != file.path && !oldFile.delete()) {
@ -463,13 +452,19 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
finish() finish()
} }
.show() .show()
} else { return@executeApiAsync
}
}
val commitMessageRes = if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text
lifecycleScope.launch {
commitChange(resources.getString(
commitMessageRes,
getLongName(fullPath, repoPath, editName)
)).onSuccess {
setResult(RESULT_OK, returnIntent) setResult(RESULT_OK, returnIntent)
finish() finish()
} }
} else {
setResult(RESULT_OK, returnIntent)
finish()
} }
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -6,9 +6,14 @@ package com.zeapo.pwdstore.git
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import com.github.ajalt.timberkt.Timber.tag import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.github.michaelbull.result.Err
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.github.michaelbull.result.Result
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.config.GitSettings import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.operation.BreakOutOfDetached import com.zeapo.pwdstore.git.operation.BreakOutOfDetached
import com.zeapo.pwdstore.git.operation.CloneOperation import com.zeapo.pwdstore.git.operation.CloneOperation
@ -17,10 +22,15 @@ import com.zeapo.pwdstore.git.operation.PullOperation
import com.zeapo.pwdstore.git.operation.PushOperation import com.zeapo.pwdstore.git.operation.PushOperation
import com.zeapo.pwdstore.git.operation.ResetToRemoteOperation import com.zeapo.pwdstore.git.operation.ResetToRemoteOperation
import com.zeapo.pwdstore.git.operation.SyncOperation import com.zeapo.pwdstore.git.operation.SyncOperation
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.sharedPrefs
import net.schmizz.sshj.common.DisconnectReason
import net.schmizz.sshj.common.SSHException
import net.schmizz.sshj.userauth.UserAuthException
/** /**
* Abstract AppCompatActivity that holds some information that is commonly shared across git-related * Abstract [AppCompatActivity] that holds some information that is commonly shared across git-related
* tasks and makes sense to be held here. * tasks and makes sense to be held here.
*/ */
abstract class BaseGitActivity : AppCompatActivity() { abstract class BaseGitActivity : AppCompatActivity() {
@ -39,33 +49,77 @@ abstract class BaseGitActivity : AppCompatActivity() {
* Attempt to launch the requested Git operation. * Attempt to launch the requested Git operation.
* @param operation The type of git operation to launch * @param operation The type of git operation to launch
*/ */
suspend fun launchGitOperation(operation: Int) { suspend fun launchGitOperation(operation: Int): Result<Unit, Throwable> {
if (GitSettings.url == null) { if (GitSettings.url == null) {
setResult(RESULT_CANCELED) return Err(IllegalStateException("Git url is not set!"))
finish()
return
} }
try {
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory())
val op = when (operation) { val op = when (operation) {
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, GitSettings.url!!, this) REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(this, GitSettings.url!!)
REQUEST_PULL -> PullOperation(localDir, this) REQUEST_PULL -> PullOperation(this)
REQUEST_PUSH -> PushOperation(localDir, this) REQUEST_PUSH -> PushOperation(this)
REQUEST_SYNC -> SyncOperation(localDir, this) REQUEST_SYNC -> SyncOperation(this)
BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this) BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(this)
REQUEST_RESET -> ResetToRemoteOperation(localDir, this) REQUEST_RESET -> ResetToRemoteOperation(this)
else -> { else -> {
tag(TAG).e { "Operation not recognized : $operation" } tag(TAG).e { "Operation not recognized : $operation" }
setResult(RESULT_CANCELED) return Err(IllegalArgumentException("$operation is not a valid Git operation"))
}
}
return op.executeAfterAuthentication(GitSettings.authMode)
}
fun finishOnSuccessHandler(@Suppress("UNUSED_PARAMETER") nothing: Unit) {
finish() finish()
return }
fun finishAfterPromptOnErrorHandler(err: Throwable) {
val error = rootCauseException(err)
if (!isExplicitlyUserInitiatedError(error)) {
getEncryptedPrefs("git_operation").edit {
remove(PreferenceKeys.HTTPS_PASSWORD)
}
sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) }
d(error)
MaterialAlertDialogBuilder(this)
.setTitle(resources.getString(R.string.jgit_error_dialog_title))
.setMessage(ErrorMessages[error])
.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ ->
finish()
}.show()
} }
} }
op.executeAfterAuthentication(GitSettings.authMode)
} catch (e: Exception) { /**
e.printStackTrace() * Check if a given [Throwable] is the result of an error caused by the user cancelling the
MaterialAlertDialogBuilder(this).setMessage(e.message).show() * operation.
*/
private fun isExplicitlyUserInitiatedError(throwable: Throwable): Boolean {
var cause: Throwable? = throwable
while (cause != null) {
if (cause is SSHException &&
cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER)
return true
cause = cause.cause
} }
return false
}
/**
* Get the real root cause of a [Throwable] by traversing until known wrapping exceptions are no
* longer found.
*/
private fun rootCauseException(throwable: Throwable): Throwable {
var rootCause = throwable
// JGit's TransportException hides the more helpful SSHJ exceptions.
// Also, SSHJ's UserAuthException about exhausting available authentication methods hides
// more useful exceptions.
while ((rootCause is org.eclipse.jgit.errors.TransportException ||
rootCause is org.eclipse.jgit.api.errors.TransportException ||
(rootCause is UserAuthException &&
rootCause.message == "Exhausted available authentication methods"))) {
rootCause = rootCause.cause ?: break
}
return rootCause
} }
companion object { companion object {

View file

@ -5,25 +5,18 @@
package com.zeapo.pwdstore.git package com.zeapo.pwdstore.git
import android.app.Activity
import android.content.Intent
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.github.ajalt.timberkt.e
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.GitException.PullException import com.zeapo.pwdstore.git.GitException.PullException
import com.zeapo.pwdstore.git.GitException.PushException import com.zeapo.pwdstore.git.GitException.PushException
import com.zeapo.pwdstore.git.config.GitSettings import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.operation.GitOperation import com.zeapo.pwdstore.git.operation.GitOperation
import com.zeapo.pwdstore.git.sshj.SshjSessionFactory
import com.zeapo.pwdstore.utils.Result
import com.zeapo.pwdstore.utils.snackbar import com.zeapo.pwdstore.utils.snackbar
import com.github.michaelbull.result.Result
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.schmizz.sshj.common.DisconnectReason
import net.schmizz.sshj.common.SSHException
import net.schmizz.sshj.userauth.UserAuthException
import org.eclipse.jgit.api.CommitCommand import org.eclipse.jgit.api.CommitCommand
import org.eclipse.jgit.api.PullCommand import org.eclipse.jgit.api.PullCommand
import org.eclipse.jgit.api.PushCommand import org.eclipse.jgit.api.PushCommand
@ -31,25 +24,20 @@ import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.api.StatusCommand import org.eclipse.jgit.api.StatusCommand
import org.eclipse.jgit.lib.PersonIdent import org.eclipse.jgit.lib.PersonIdent
import org.eclipse.jgit.transport.RemoteRefUpdate import org.eclipse.jgit.transport.RemoteRefUpdate
import org.eclipse.jgit.transport.SshSessionFactory
class GitCommandExecutor( class GitCommandExecutor(
private val activity: FragmentActivity, private val activity: FragmentActivity,
private val operation: GitOperation, private val operation: GitOperation,
private val finishWithResultOnEnd: Intent? = Intent(),
private val finishActivityOnEnd: Boolean = true,
) { ) {
suspend fun execute() { suspend fun execute(): Result<Unit, Throwable> {
operation.setCredentialProvider()
val snackbar = activity.snackbar( val snackbar = activity.snackbar(
message = activity.resources.getString(R.string.git_operation_running), message = activity.resources.getString(R.string.git_operation_running),
length = Snackbar.LENGTH_INDEFINITE, length = Snackbar.LENGTH_INDEFINITE,
) )
// Count the number of uncommitted files // Count the number of uncommitted files
var nbChanges = 0 var nbChanges = 0
var operationResult: Result = Result.Ok return com.github.michaelbull.result.runCatching {
try {
for (command in operation.commands) { for (command in operation.commands) {
when (command) { when (command) {
is StatusCommand -> { is StatusCommand -> {
@ -74,7 +62,7 @@ class GitCommandExecutor(
} }
val rr = result.rebaseResult val rr = result.rebaseResult
if (rr.status === RebaseResult.Status.STOPPED) { if (rr.status === RebaseResult.Status.STOPPED) {
operationResult = Result.Err(PullException.PullRebaseFailed) throw PullException.PullRebaseFailed
} }
} }
is PushCommand -> { is PushCommand -> {
@ -84,15 +72,15 @@ class GitCommandExecutor(
for (result in results) { for (result in results) {
// Code imported (modified) from Gerrit PushOp, license Apache v2 // Code imported (modified) from Gerrit PushOp, license Apache v2
for (rru in result.remoteUpdates) { for (rru in result.remoteUpdates) {
val error = when (rru.status) { when (rru.status) {
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> PushException.NonFastForward RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> throw PushException.NonFastForward
RemoteRefUpdate.Status.REJECTED_NODELETE, RemoteRefUpdate.Status.REJECTED_NODELETE,
RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED, RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED,
RemoteRefUpdate.Status.NON_EXISTING, RemoteRefUpdate.Status.NON_EXISTING,
RemoteRefUpdate.Status.NOT_ATTEMPTED, RemoteRefUpdate.Status.NOT_ATTEMPTED,
-> PushException.Generic(rru.status.name) -> throw PushException.Generic(rru.status.name)
RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> { RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> {
if ("non-fast-forward" == rru.message) { throw if ("non-fast-forward" == rru.message) {
PushException.RemoteRejected PushException.RemoteRejected
} else { } else {
PushException.Generic(rru.message) PushException.Generic(rru.message)
@ -106,13 +94,7 @@ class GitCommandExecutor(
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
null
} }
else -> null
}
if (error != null) {
operationResult = Result.Err(error)
} }
} }
} }
@ -124,56 +106,8 @@ class GitCommandExecutor(
} }
} }
} }
} catch (e: Exception) { }.also {
operationResult = Result.Err(e)
}
when (operationResult) {
is Result.Err -> {
activity.setResult(Activity.RESULT_CANCELED)
if (isExplicitlyUserInitiatedError(operationResult.err)) {
// Currently, this is only executed when the user cancels a password prompt
// during authentication.
if (finishActivityOnEnd) activity.finish()
} else {
e(operationResult.err)
operation.onError(rootCauseException(operationResult.err))
}
}
is Result.Ok -> {
operation.onSuccess()
activity.setResult(Activity.RESULT_OK, finishWithResultOnEnd)
if (finishActivityOnEnd) activity.finish()
}
}
snackbar.dismiss() snackbar.dismiss()
withContext(Dispatchers.IO) { }
(SshSessionFactory.getInstance() as? SshjSessionFactory)?.close()
}
SshSessionFactory.setInstance(null)
}
private fun isExplicitlyUserInitiatedError(e: Exception): Boolean {
var cause: Exception? = e
while (cause != null) {
if (cause is SSHException &&
cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER)
return true
cause = cause.cause as? Exception
}
return false
}
private fun rootCauseException(e: Exception): Exception {
var rootCause = e
// JGit's TransportException hides the more helpful SSHJ exceptions.
// Also, SSHJ's UserAuthException about exhausting available authentication methods hides
// more useful exceptions.
while ((rootCause is org.eclipse.jgit.errors.TransportException ||
rootCause is org.eclipse.jgit.api.errors.TransportException ||
(rootCause is UserAuthException &&
rootCause.message == "Exhausted available authentication methods"))) {
rootCause = rootCause.cause as? Exception ?: break
}
return rootCause
} }
} }

View file

@ -11,6 +11,7 @@ import android.util.Patterns
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.github.michaelbull.result.fold
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
@ -75,8 +76,34 @@ class GitConfigActivity : BaseGitActivity() {
e(ex) { "Failed to start GitLogActivity" } e(ex) { "Failed to start GitLogActivity" }
} }
} }
binding.gitAbortRebase.setOnClickListener { lifecycleScope.launch { launchGitOperation(BREAK_OUT_OF_DETACHED) } } binding.gitAbortRebase.setOnClickListener {
binding.gitResetToRemote.setOnClickListener { lifecycleScope.launch { launchGitOperation(REQUEST_RESET) } } lifecycleScope.launch {
launchGitOperation(BREAK_OUT_OF_DETACHED).fold(
success = {
MaterialAlertDialogBuilder(this@GitConfigActivity)
.setTitle(resources.getString(R.string.git_abort_and_push_title))
.setMessage(resources.getString(
R.string.git_break_out_of_detached_success,
GitSettings.branch,
"conflicting-${GitSettings.branch}-...",
))
.setOnCancelListener { finish() }
.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ ->
finish()
}.show()
},
failure = ::finishAfterPromptOnErrorHandler,
)
}
}
binding.gitResetToRemote.setOnClickListener {
lifecycleScope.launch {
launchGitOperation(REQUEST_RESET).fold(
success = ::finishOnSuccessHandler,
failure = ::finishAfterPromptOnErrorHandler,
)
}
}
} }
/** /**

View file

@ -1,79 +0,0 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.git
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.utils.PasswordRepository
import kotlinx.coroutines.launch
open class GitOperationActivity : BaseGitActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (val reqCode = intent.extras?.getInt(REQUEST_ARG_OP)) {
REQUEST_PULL -> lifecycleScope.launch { syncRepository(REQUEST_PULL) }
REQUEST_PUSH -> lifecycleScope.launch { syncRepository(REQUEST_PUSH) }
REQUEST_SYNC -> lifecycleScope.launch { syncRepository(REQUEST_SYNC) }
else -> {
throw IllegalArgumentException("Invalid request code: $reqCode")
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.git_clone, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.user_pref -> try {
val intent = Intent(this, UserPreference::class.java)
startActivity(intent)
true
} catch (e: Exception) {
println("Exception caught :(")
e.printStackTrace()
false
}
else -> super.onOptionsItemSelected(item)
}
}
/**
* Syncs the local repository with the remote one (either pull or push)
*
* @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH
*/
private suspend fun syncRepository(operation: Int) {
if (GitSettings.url.isNullOrEmpty())
MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.set_information_dialog_text))
.setPositiveButton(getString(R.string.dialog_positive)) { _, _ ->
val intent = Intent(this, UserPreference::class.java)
startActivityForResult(intent, REQUEST_PULL)
}
.setNegativeButton(getString(R.string.dialog_negative)) { _, _ ->
// do nothing :(
setResult(RESULT_OK)
finish()
}
.show()
else {
// check that the remote origin is here, else add it
PasswordRepository.addRemote("origin", GitSettings.url!!, true)
launchGitOperation(operation)
}
}
}

View file

@ -9,6 +9,7 @@ import android.os.Handler
import android.view.View import android.view.View
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.michaelbull.result.fold
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
@ -122,7 +123,10 @@ class GitServerConfigActivity : BaseGitActivity() {
localDir.deleteRecursively() localDir.deleteRecursively()
} }
snackbar.dismiss() snackbar.dismiss()
launchGitOperation(REQUEST_CLONE) launchGitOperation(REQUEST_CLONE).fold(
success = ::finishOnSuccessHandler,
failure = ::finishAfterPromptOnErrorHandler,
)
} }
} catch (e: IOException) { } catch (e: IOException) {
// TODO Handle the exception correctly if we are unable to delete the directory... // TODO Handle the exception correctly if we are unable to delete the directory...
@ -153,7 +157,12 @@ class GitServerConfigActivity : BaseGitActivity() {
e.printStackTrace() e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show() MaterialAlertDialogBuilder(this).setMessage(e.message).show()
} }
lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) } lifecycleScope.launch {
launchGitOperation(REQUEST_CLONE).fold(
success = ::finishOnSuccessHandler,
failure = ::finishAfterPromptOnErrorHandler,
)
}
} }
} }
} }

View file

@ -7,46 +7,30 @@ package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.GitCommandExecutor
import java.io.File
import org.eclipse.jgit.api.RebaseCommand import org.eclipse.jgit.api.RebaseCommand
class BreakOutOfDetached(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { class BreakOutOfDetached(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
private val branchName = "conflicting-$remoteBranch-${System.currentTimeMillis()}"
override val commands = arrayOf( override val commands = arrayOf(
// abort the rebase // abort the rebase
git.rebase().setOperation(RebaseCommand.Operation.ABORT), git.rebase().setOperation(RebaseCommand.Operation.ABORT),
// git checkout -b conflict-branch // git checkout -b conflict-branch
git.checkout().setCreateBranch(true).setName(branchName), git.checkout().setCreateBranch(true).setName("conflicting-$remoteBranch-${System.currentTimeMillis()}"),
// push the changes // push the changes
git.push().setRemote("origin"), git.push().setRemote("origin"),
// switch back to ${gitBranch} // switch back to ${gitBranch}
git.checkout().setName(remoteBranch), git.checkout().setName(remoteBranch),
) )
override suspend fun execute() { override fun preExecute() = if (!git.repository.repositoryState.isRebasing) {
if (!git.repository.repositoryState.isRebasing) {
MaterialAlertDialogBuilder(callingActivity) MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title)) .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title))
.setMessage("The repository is not rebasing, no need to push to another branch") .setMessage(callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded))
.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)) { _, _ -> .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
callingActivity.finish() callingActivity.finish()
}.show() }.show()
false
} else {
true
} }
} }

View file

@ -5,34 +5,18 @@
package com.zeapo.pwdstore.git.operation package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity 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.Git
import org.eclipse.jgit.api.GitCommand import org.eclipse.jgit.api.GitCommand
/** /**
* Creates a new clone operation * Creates a new clone operation
* *
* @param fileDir the git working tree directory
* @param uri URL to clone the repository from * @param uri URL to clone the repository from
* @param callingActivity the calling activity * @param callingActivity the calling activity
*/ */
class CloneOperation(fileDir: File, uri: String, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) { class CloneOperation(callingActivity: AppCompatActivity, uri: String) : GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> = arrayOf( override val commands: Array<GitCommand<out Any>> = arrayOf(
Git.cloneRepository().setBranch(remoteBranch).setDirectory(repository?.workTree).setURI(uri), Git.cloneRepository().setBranch(remoteBranch).setDirectory(repository.workTree).setURI(uri),
) )
override suspend fun execute() {
GitCommandExecutor(callingActivity, this, finishActivityOnEnd = false).execute()
}
override fun onSuccess() {
callingActivity.finish()
}
override fun onError(err: Exception) {
finishFromErrorDialog = false
super.onError(err)
}
} }

View file

@ -6,30 +6,27 @@ package com.zeapo.pwdstore.git.operation
import android.content.Intent import android.content.Intent
import android.widget.Toast import android.widget.Toast
import androidx.annotation.CallSuper
import androidx.core.content.edit
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.github.ajalt.timberkt.Timber.d import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.ErrorMessages import com.zeapo.pwdstore.git.GitCommandExecutor
import com.zeapo.pwdstore.git.config.AuthMode import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.sshj.InteractivePasswordFinder
import com.zeapo.pwdstore.git.sshj.SshAuthData import com.zeapo.pwdstore.git.sshj.SshAuthData
import com.zeapo.pwdstore.git.sshj.SshKey import com.zeapo.pwdstore.git.sshj.SshKey
import com.zeapo.pwdstore.git.sshj.SshjSessionFactory import com.zeapo.pwdstore.git.sshj.SshjSessionFactory
import com.zeapo.pwdstore.utils.BiometricAuthenticator import com.zeapo.pwdstore.utils.BiometricAuthenticator
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.schmizz.sshj.common.DisconnectReason
import net.schmizz.sshj.common.SSHException
import net.schmizz.sshj.userauth.password.PasswordFinder import net.schmizz.sshj.userauth.password.PasswordFinder
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand import org.eclipse.jgit.api.GitCommand
@ -37,24 +34,22 @@ import org.eclipse.jgit.api.TransportCommand
import org.eclipse.jgit.errors.UnsupportedCredentialItem import org.eclipse.jgit.errors.UnsupportedCredentialItem
import org.eclipse.jgit.transport.CredentialItem import org.eclipse.jgit.transport.CredentialItem
import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.SshSessionFactory import org.eclipse.jgit.transport.SshTransport
import org.eclipse.jgit.transport.Transport
import org.eclipse.jgit.transport.URIish import org.eclipse.jgit.transport.URIish
const val ANDROID_KEYSTORE_ALIAS_SSH_KEY = "ssh_key"
/** /**
* Creates a new git operation * Creates a new git operation
* *
* @param gitDir the git working tree directory
* @param callingActivity the calling activity * @param callingActivity the calling activity
*/ */
abstract class GitOperation(gitDir: File, internal val callingActivity: FragmentActivity) { abstract class GitOperation(protected val callingActivity: FragmentActivity) {
abstract val commands: Array<GitCommand<out Any>> abstract val commands: Array<GitCommand<out Any>>
private var provider: CredentialsProvider? = null
private val hostKeyFile = callingActivity.filesDir.resolve(".host_key") private val hostKeyFile = callingActivity.filesDir.resolve(".host_key")
protected var finishFromErrorDialog = true private var sshSessionFactory: SshjSessionFactory? = null
protected val repository = PasswordRepository.getRepository(gitDir)
protected val repository = PasswordRepository.getRepository(null)!!
protected val git = Git(repository) protected val git = Git(repository)
protected val remoteBranch = GitSettings.branch protected val remoteBranch = GitSettings.branch
@ -90,27 +85,6 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
} }
} }
private fun withPasswordAuthentication(passwordFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(SshAuthData.Password(passwordFinder), hostKeyFile)
SshSessionFactory.setInstance(sessionFactory)
this.provider = HttpsCredentialsProvider(passwordFinder)
return this
}
private fun withSshKeyAuthentication(passphraseFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(SshAuthData.SshKey(passphraseFinder), hostKeyFile)
SshSessionFactory.setInstance(sessionFactory)
this.provider = null
return this
}
private fun withOpenKeychainAuthentication(activity: FragmentActivity): GitOperation {
val sessionFactory = SshjSessionFactory(SshAuthData.OpenKeychain(activity), hostKeyFile)
SshSessionFactory.setInstance(sessionFactory)
this.provider = null
return this
}
private fun getSshKey(make: Boolean) { private fun getSshKey(make: Boolean) {
try { try {
// Ask the UserPreference to provide us with the ssh-key // Ask the UserPreference to provide us with the ssh-key
@ -124,16 +98,30 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
} }
} }
fun setCredentialProvider() { private fun registerAuthProviders(authData: SshAuthData, credentialsProvider: CredentialsProvider? = null) {
provider?.let { credentialsProvider -> sshSessionFactory = SshjSessionFactory(authData, hostKeyFile)
commands.filterIsInstance<TransportCommand<*, *>>().forEach { it.setCredentialsProvider(credentialsProvider) } commands.filterIsInstance<TransportCommand<*, *>>().forEach { command ->
command.setTransportConfigCallback { transport: Transport ->
(transport as? SshTransport)?.sshSessionFactory = sshSessionFactory
credentialsProvider?.let { transport.credentialsProvider = it }
}
} }
} }
/** /**
* Executes the GitCommand in an async task * Executes the GitCommand in an async task.
*/ */
abstract suspend fun execute() suspend fun execute(): Result<Unit, Throwable> {
if (!preExecute()) {
return Ok(Unit)
}
val operationResult = GitCommandExecutor(
callingActivity,
this,
).execute()
postExecute()
return operationResult
}
private fun onMissingSshKeyFile() { private fun onMissingSshKeyFile() {
MaterialAlertDialogBuilder(callingActivity) MaterialAlertDialogBuilder(callingActivity)
@ -151,9 +139,7 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
}.show() }.show()
} }
suspend fun executeAfterAuthentication( suspend fun executeAfterAuthentication(authMode: AuthMode): Result<Unit, Throwable> {
authMode: AuthMode,
) {
when (authMode) { when (authMode) {
AuthMode.SshKey -> if (SshKey.exists) { AuthMode.SshKey -> if (SshKey.exists) {
if (SshKey.mustAuthenticate) { if (SshKey.mustAuthenticate) {
@ -167,9 +153,12 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
} }
when (result) { when (result) {
is BiometricAuthenticator.Result.Success -> { is BiometricAuthenticator.Result.Success -> {
withSshKeyAuthentication(CredentialFinder(callingActivity, authMode)).execute() registerAuthProviders(
SshAuthData.SshKey(CredentialFinder(callingActivity, AuthMode.SshKey)))
}
is BiometricAuthenticator.Result.Cancelled -> {
return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER))
} }
is BiometricAuthenticator.Result.Cancelled -> callingActivity.finish()
is BiometricAuthenticator.Result.Failure -> { is BiometricAuthenticator.Result.Failure -> {
throw IllegalStateException("Biometric authentication failures should be ignored") throw IllegalStateException("Biometric authentication failures should be ignored")
} }
@ -183,41 +172,36 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
} }
} }
} else { } else {
withSshKeyAuthentication(CredentialFinder(callingActivity, authMode)).execute() registerAuthProviders(SshAuthData.SshKey(CredentialFinder(callingActivity, AuthMode.SshKey)))
} }
} else { } else {
onMissingSshKeyFile() onMissingSshKeyFile()
} }
AuthMode.OpenKeychain -> withOpenKeychainAuthentication(callingActivity).execute() AuthMode.OpenKeychain -> registerAuthProviders(SshAuthData.OpenKeychain(callingActivity))
AuthMode.Password -> withPasswordAuthentication( AuthMode.Password -> {
CredentialFinder(callingActivity, authMode)).execute() val credentialFinder = CredentialFinder(callingActivity, AuthMode.Password)
AuthMode.None -> execute() val httpsCredentialProvider = HttpsCredentialsProvider(credentialFinder)
registerAuthProviders(
SshAuthData.Password(CredentialFinder(callingActivity, AuthMode.Password)),
httpsCredentialProvider)
} }
AuthMode.None -> {
}
}
return execute()
} }
/** /**
* Action to execute on error * Called before execution of the Git operation.
* Return false to cancel.
*/ */
@CallSuper open fun preExecute() = true
open fun onError(err: Exception) {
// Clear various auth related fields on failure
callingActivity.getEncryptedPrefs("git_operation").edit {
remove(PreferenceKeys.HTTPS_PASSWORD)
}
callingActivity.sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) }
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)) { _, _ ->
if (finishFromErrorDialog) callingActivity.finish()
}.show()
}
/** private suspend fun postExecute() {
* Action to execute on success withContext(Dispatchers.IO) {
*/ sshSessionFactory?.close()
open fun onSuccess() {} }
}
companion object { companion object {

View file

@ -5,24 +5,11 @@
package com.zeapo.pwdstore.git.operation package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity 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 import org.eclipse.jgit.api.GitCommand
/** class PullOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
* 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( override val commands: Array<GitCommand<out Any>> = arrayOf(
Git(repository).pull().setRebase(true).setRemote("origin"), git.pull().setRebase(true).setRemote("origin"),
) )
override suspend fun execute() {
GitCommandExecutor(callingActivity, this).execute()
}
} }

View file

@ -5,25 +5,11 @@
package com.zeapo.pwdstore.git.operation package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity 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 import org.eclipse.jgit.api.GitCommand
/** class PushOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
* 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( override val commands: Array<GitCommand<out Any>> = arrayOf(
Git(repository).push().setPushAll().setRemote("origin"), git.push().setPushAll().setRemote("origin"),
) )
override suspend fun execute() {
setCredentialProvider()
GitCommandExecutor(callingActivity, this).execute()
}
} }

View file

@ -5,17 +5,9 @@
package com.zeapo.pwdstore.git.operation package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.zeapo.pwdstore.git.GitCommandExecutor
import java.io.File
import org.eclipse.jgit.api.ResetCommand import org.eclipse.jgit.api.ResetCommand
/** class ResetToRemoteOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
* 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( override val commands = arrayOf(
// Stage all files // Stage all files
@ -28,8 +20,4 @@ class ResetToRemoteOperation(fileDir: File, callingActivity: AppCompatActivity)
// branches from 'master' to anything else. // branches from 'master' to anything else.
git.branchCreate().setName(remoteBranch).setForce(true), git.branchCreate().setName(remoteBranch).setForce(true),
) )
override suspend fun execute() {
GitCommandExecutor(callingActivity, this).execute()
}
} }

View file

@ -5,16 +5,8 @@
package com.zeapo.pwdstore.git.operation package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.zeapo.pwdstore.git.GitCommandExecutor
import java.io.File
/** class SyncOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
* 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( override val commands = arrayOf(
// Stage all files // Stage all files
@ -28,8 +20,4 @@ class SyncOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOper
// Push it all back // Push it all back
git.push().setPushAll().setRemote("origin"), git.push().setPushAll().setRemote("origin"),
) )
override suspend fun execute() {
GitCommandExecutor(callingActivity, this).execute()
}
} }

View file

@ -77,7 +77,6 @@ class FolderCreationDialogFragment : DialogFragment() {
R.string.git_commit_gpg_id, R.string.git_commit_gpg_id,
BasePgpActivity.getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) BasePgpActivity.getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
), ),
finishActivityOnEnd = false,
) )
dismiss() dismiss()
} }

View file

@ -7,7 +7,6 @@ package com.zeapo.pwdstore.utils
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.util.Base64 import android.util.Base64
@ -24,8 +23,10 @@ import androidx.preference.PreferenceManager
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey import androidx.security.crypto.MasterKey
import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.d
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.git.GitCommandExecutor import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.operation.GitOperation import com.zeapo.pwdstore.git.operation.GitOperation
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory
import java.io.File import java.io.File
@ -59,6 +60,7 @@ fun FragmentActivity.snackbar(
length: Int = Snackbar.LENGTH_SHORT, length: Int = Snackbar.LENGTH_SHORT,
): Snackbar { ): Snackbar {
val snackbar = Snackbar.make(view, message, length) val snackbar = Snackbar.make(view, message, length)
snackbar.anchorView = findViewById(R.id.fab)
snackbar.show() snackbar.show()
return snackbar return snackbar
} }
@ -108,17 +110,11 @@ fun SharedPreferences.getString(key: String): String? = getString(key, null)
suspend fun FragmentActivity.commitChange( suspend fun FragmentActivity.commitChange(
message: String, message: String,
finishWithResultOnEnd: Intent? = null, ): Result<Unit, Throwable> {
finishActivityOnEnd: Boolean = true,
) {
if (!PasswordRepository.isGitRepo()) { if (!PasswordRepository.isGitRepo()) {
if (finishWithResultOnEnd != null) { return Ok(Unit)
setResult(FragmentActivity.RESULT_OK, finishWithResultOnEnd)
finish()
} }
return return object : GitOperation(this@commitChange) {
}
object : GitOperation(getRepositoryDirectory(), this@commitChange) {
override val commands = arrayOf( override val commands = arrayOf(
// Stage all files // Stage all files
git.add().addFilepattern("."), git.add().addFilepattern("."),
@ -128,14 +124,9 @@ suspend fun FragmentActivity.commitChange(
git.commit().setAll(true).setMessage(message), git.commit().setAll(true).setMessage(message),
) )
override suspend fun execute() { override fun preExecute(): Boolean {
d { "Comitting with message: '$message'" } d { "Committing with message: '$message'" }
GitCommandExecutor( return true
this@commitChange,
this,
finishWithResultOnEnd,
finishActivityOnEnd,
).execute()
} }
}.execute() }.execute()
} }

View file

@ -1,16 +0,0 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.utils
/**
* Emulates the Rust Result enum but without returning a value in the [Ok] case.
* https://doc.rust-lang.org/std/result/enum.Result.html
*/
sealed class Result {
object Ok : Result()
data class Err(val err: Exception) : Result()
}

View file

@ -11,7 +11,7 @@
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
android:padding="@dimen/activity_horizontal_margin" android:padding="@dimen/activity_horizontal_margin"
tools:background="@color/white" tools:background="@color/white"
tools:context="com.zeapo.pwdstore.git.GitOperationActivity"> tools:context="com.zeapo.pwdstore.git.GitServerConfigActivity">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -1,15 +0,0 @@
<!--
~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
~ SPDX-License-Identifier: GPL-3.0-only
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:pwstore="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.zeapo.pwdstore.git.GitServerConfigActivity">
<item
android:id="@+id/user_pref"
android:orderInCategory="100"
android:title="@string/action_settings"
pwstore:showAsAction="never" />
</menu>

View file

@ -402,6 +402,8 @@
<string name="git_push_other_error">Remote rejected non-fast-forward push. Check receive.denyNonFastForwards variable in config file of destination repository.</string> <string name="git_push_other_error">Remote rejected non-fast-forward push. Check receive.denyNonFastForwards variable in config file of destination repository.</string>
<string name="git_unknown_host">Unknown host: %1$s</string> <string name="git_unknown_host">Unknown host: %1$s</string>
<string name="git_operation_running">Running git operation…</string> <string name="git_operation_running">Running git operation…</string>
<string name="git_break_out_of_detached_success">There was a conflict when trying to rebase. Your local %1$s branch was pushed to another branch named %2$s\n Use this branch to resolve conflict on your computer</string>
<string name="git_break_out_of_detached_unneeded">The repository is not rebasing, no need to push to another branch</string>
<!-- OpenKeychain not installed --> <!-- OpenKeychain not installed -->
<string name="openkeychain_not_installed_title">OpenKeychain not installed</string> <string name="openkeychain_not_installed_title">OpenKeychain not installed</string>

View file

@ -53,7 +53,7 @@ subprojects {
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn", "-Xallow-result-return-type")
languageVersion = "1.4" languageVersion = "1.4"
} }
} }

View file

@ -58,6 +58,7 @@ object Dependencies {
const val eddsa = "net.i2p.crypto:eddsa:0.3.0" const val eddsa = "net.i2p.crypto:eddsa:0.3.0"
const val fastscroll = "me.zhanghai.android.fastscroll:library:1.1.4" const val fastscroll = "me.zhanghai.android.fastscroll:library:1.1.4"
const val jgit = "org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r" const val jgit = "org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r"
const val kotlin_result = "com.michael-bull.kotlin-result:kotlin-result:1.1.9"
const val leakcanary = "com.squareup.leakcanary:leakcanary-android:2.4" const val leakcanary = "com.squareup.leakcanary:leakcanary-android:2.4"
const val plumber = "com.squareup.leakcanary:plumber-android:2.4" const val plumber = "com.squareup.leakcanary:plumber-android:2.4"
const val sshj = "com.hierynomus:sshj:0.30.0" const val sshj = "com.hierynomus:sshj:0.30.0"