aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
diff options
context:
space:
mode:
authorHarsh Shandilya <msfjarvis@gmail.com>2020-04-17 18:36:07 +0530
committerGitHub <noreply@github.com>2020-04-17 18:36:07 +0530
commitb94b52a42ddc2325b539d0956fd0adcf35308b52 (patch)
tree21f35526cfc444db82e1b4b08d083e49a3c1843b /app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
parent4ffd7ed9bffa5139277ffb91de5a69f2b714222c (diff)
downloadHardenedPass-b94b52a42ddc2325b539d0956fd0adcf35308b52.tar.gz
HardenedPass-b94b52a42ddc2325b539d0956fd0adcf35308b52.zip
Refactor Git related activities (#685)
* Refactor git logic into separate parts * Extract hardcoded strings * Add KDoc to updateHostname, remove unused field * Cleanups * Fix dialog message * Wire in repository clone flow * spotless * Remove unused method * Cleanup GitActivity - Rename to GitOperationActivity. - Ensure identityBuilder is always closed regardless of what fragment uses it. - Remove hardcoded "Operation" strings and replace with REQUEST_ARG_OP. - Apply a transparent theme to GitOperationActivity make the UI less jarring. * Tweak some stupidly worded dialog messages As pointed out in #629, these strings are shoddily worded and do not express any clear intent to the user, leaving them confused and angry. * GitOperationActivity: wrap Context to ensure right theme is used * spotless * undo build.gradle change * Use correct parent theme, remove now useless wrapping * GitServerConfigActivity: fix repository clone flow * temp: disable leakcanary framework leaks on Samsung are pissing me off * Make system bars transparent in git activity * Tweak HTTPS password layout * Unhardcode wrong passphrase string * Store SSH passphrase in EncryptedSharedPreferences Also revamp the dialog to look a bit better * Implement support for remembering HTTPS password Fixes #521 * Try to patch HTTPS remote creation logic * Update security-crypto * Clear saved passphrase/password on auth failure * Revert "Update security-crypto" Broken on R DP2.1 This reverts commit 4b20371dd42c512a3dd3b759859abb6c1ffd2961. * Revert "temp: disable leakcanary" This reverts commit 2db7d41bd67b79c6dc8c5b359a7b27100379f45f. * Update CHANGELOG * Remove spacer * Remove useless override * Wrap git server activity in a ScrollView * GitOperation: always finish calling activity when dialogs are dismissed * Wipe saved password/passphrase when hostname changes * Don't commit prefs updates * Don't call listFiles excessively * Finish activity after saving configuration * Make ConnectionMode and Protocol enum classes * Change SSH key passphrase key, don't wipe on host change * Reimplement BaseGitActivity.updateUrl (was updateHostname) * Use SharedPreferences.edit KTX extension * Disable inapplicable connection modes depending on scheme * BaseGitActivity: annotate onDestroy with CallSuper We'll leak the identityBuilder connection otherwise * Move input hack for AlertDialog into an extension function We re-use this in many places * Fix protocol/mode toggle issue and consistenly name options * Fix a crash when opening GitServerConfigActivity without a repo * Fix OpenKeychain callbacks by moving onActivityResult to BaseGitActivity * Run spotlessApply Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> Co-authored-by: Fabian Henneke <fabian@henneke.me>
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt212
1 files changed, 212 insertions, 0 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
new file mode 100644
index 00000000..36dd95c4
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
@@ -0,0 +1,212 @@
+/*
+ * 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.content.SharedPreferences
+import android.os.Bundle
+import android.view.MenuItem
+import androidx.annotation.CallSuper
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.edit
+import androidx.core.text.isDigitsOnly
+import androidx.preference.PreferenceManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.zeapo.pwdstore.git.config.ConnectionMode
+import com.zeapo.pwdstore.git.config.Protocol
+import com.zeapo.pwdstore.git.config.SshApiSessionFactory
+import com.zeapo.pwdstore.utils.PasswordRepository
+import com.zeapo.pwdstore.utils.getEncryptedPrefs
+import java.io.File
+import timber.log.Timber
+
+/**
+ * Abstract AppCompatActivity that holds some information that is commonly shared across git-related
+ * tasks and makes sense to be held here.
+ */
+abstract class BaseGitActivity : AppCompatActivity() {
+ lateinit var protocol: Protocol
+ lateinit var connectionMode: ConnectionMode
+ lateinit var url: String
+ lateinit var serverHostname: String
+ lateinit var serverPort: String
+ lateinit var serverUser: String
+ lateinit var serverPath: String
+ lateinit var username: String
+ lateinit var email: String
+ var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null
+ var identity: SshApiSessionFactory.ApiIdentity? = null
+ lateinit var settings: SharedPreferences
+ private set
+ private lateinit var encryptedSettings: SharedPreferences
+
+ @CallSuper
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ settings = PreferenceManager.getDefaultSharedPreferences(this)
+ encryptedSettings = getEncryptedPrefs("git_operation")
+ protocol = Protocol.fromString(settings.getString("git_remote_protocol", null))
+ connectionMode = ConnectionMode.fromString(settings.getString("git_remote_auth", null))
+ serverHostname = settings.getString("git_remote_server", null) ?: ""
+ serverPort = settings.getString("git_remote_port", null) ?: ""
+ serverUser = settings.getString("git_remote_username", null) ?: ""
+ serverPath = settings.getString("git_remote_location", null) ?: ""
+ username = settings.getString("git_config_user_name", null) ?: ""
+ email = settings.getString("git_config_user_email", null) ?: ""
+ updateUrl()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ finish()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ @CallSuper
+ override fun onDestroy() {
+ // Do not leak the service connection
+ if (identityBuilder != null) {
+ identityBuilder!!.close()
+ identityBuilder = null
+ }
+ super.onDestroy()
+ }
+
+ /**
+ * Update the [url] field with the values that build it up. This function returns a boolean
+ * indicating whether or not the values are likely valid or not, and only adds the `origin`
+ * remote when it is. This check is not perfect, it is mostly meant to catch typos.
+ */
+ fun updateUrl(): Boolean {
+ if (serverHostname.isEmpty() || !serverPort.isDigitsOnly())
+ return false
+
+ val previousUrl = if (::url.isInitialized) url else ""
+ val hostnamePart = serverHostname
+ val pathPart = if (serverPath.startsWith('/')) serverPath else "/$serverPath"
+ url = when (protocol) {
+ Protocol.Ssh -> {
+ val userPart = if (serverUser.isEmpty()) "" else "$serverUser@"
+ val portPart =
+ if (serverPort == "22" || serverPort.isEmpty()) "" else ":$serverPort"
+ // We have to specify the ssh scheme as this is the only way to pass a custom port.
+ "ssh://$userPart$hostnamePart$portPart$pathPart"
+ }
+ Protocol.Https -> {
+ val portPart =
+ if (serverPort == "443" || serverPort.isEmpty()) "" else ":$serverPort"
+ "https://$hostnamePart$portPart$pathPart"
+ }
+ }
+ if (PasswordRepository.isInitialized)
+ PasswordRepository.addRemote("origin", url, true)
+ // HTTPS authentication sends the password to the server, so we must wipe the password when
+ // the server is changed.
+ if (url != previousUrl && protocol == Protocol.Https)
+ encryptedSettings.edit { remove("https_password") }
+ return true
+ }
+
+ /**
+ * Attempt to launch the requested Git operation. Depending on the configured auth, it may not
+ * be possible to launch the operation immediately. In that case, this function may launch an
+ * intermediate activity instead, which will gather necessary information and post it back via
+ * onActivityResult, which will then re-call this function. This may happen multiple times,
+ * until either an error is encountered or the operation is successfully launched.
+ *
+ * @param operation The type of git operation to launch
+ */
+ fun launchGitOperation(operation: Int) {
+ val op: GitOperation
+ val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this))
+ try {
+ // Before launching the operation with OpenKeychain auth, we need to issue several requests
+ // to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents,
+ // we just need to keep calling it until it returns a completed ApiIdentity.
+ if (connectionMode == ConnectionMode.OpenKeychain && identity == null) {
+ // Lazy initialization of the IdentityBuilder
+ if (identityBuilder == null) {
+ identityBuilder = SshApiSessionFactory.IdentityBuilder(this)
+ }
+ // Try to get an ApiIdentity and bail if one is not ready yet. The builder will ensure
+ // that onActivityResult is called with operation again, which will re-invoke us here
+ identity = identityBuilder!!.tryBuild(operation)
+ if (identity == null)
+ return
+ }
+
+ op = when (operation) {
+ REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, this).setCommand(url)
+ REQUEST_PULL -> PullOperation(localDir, this).setCommand()
+ REQUEST_PUSH -> PushOperation(localDir, this).setCommand()
+ REQUEST_SYNC -> SyncOperation(localDir, this).setCommands()
+ BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this).setCommands()
+ REQUEST_RESET -> ResetToRemoteOperation(localDir, this).setCommands()
+ SshApiSessionFactory.POST_SIGNATURE -> return
+ else -> {
+ Timber.tag(TAG).e("Operation not recognized : $operation")
+ setResult(RESULT_CANCELED)
+ finish()
+ return
+ }
+ }
+ op.executeAfterAuthentication(connectionMode, serverUser,
+ File("$filesDir/.ssh_key"), identity)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ MaterialAlertDialogBuilder(this).setMessage(e.message).show()
+ }
+ }
+
+ public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ // In addition to the pre-operation-launch series of intents for OpenKeychain auth
+ // that will pass through here and back to launchGitOperation, there is one
+ // synchronous operation that happens /after/ the operation has been launched in the
+ // background thread - the actual signing of the SSH challenge. We pass through the
+ // completed signature to the ApiIdentity, which will be blocked in the other thread
+ // waiting for it.
+ if (requestCode == SshApiSessionFactory.POST_SIGNATURE && identity != null) {
+ identity!!.postSignature(data)
+
+ // If the signature failed (usually because it was cancelled), reset state
+ if (data == null) {
+ identity = null
+ identityBuilder = null
+ }
+ return
+ }
+
+ if (resultCode == RESULT_CANCELED) {
+ setResult(RESULT_CANCELED)
+ finish()
+ } else if (resultCode == RESULT_OK) {
+ // If an operation has been re-queued via this mechanism, let the
+ // IdentityBuilder attempt to extract some updated state from the intent before
+ // trying to re-launch the operation.
+ if (identityBuilder != null) {
+ identityBuilder!!.consume(data)
+ }
+ launchGitOperation(requestCode)
+ }
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ companion object {
+ const val REQUEST_ARG_OP = "OPERATION"
+ const val REQUEST_PULL = 101
+ const val REQUEST_PUSH = 102
+ const val REQUEST_CLONE = 103
+ const val REQUEST_INIT = 104
+ const val REQUEST_SYNC = 105
+ const val BREAK_OUT_OF_DETACHED = 106
+ const val REQUEST_RESET = 107
+ const val TAG = "AbstractGitActivity"
+ }
+}