diff options
41 files changed, 1026 insertions, 1071 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 2308cce8..ee59aa7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,13 @@ All notable changes to this project will be documented in this file. ### Added - Oreo Autofill support +- Securely remember HTTPS password/SSH key passphrase ### Fixed - Text input box theming - Password repository held in non-hidden storage no longer fails +- Remove ambiguous and confusing URL field in server config menu + and heavily improve UI for ease of use. ## [1.6.0] - 2020-03-20 diff --git a/app/build.gradle b/app/build.gradle index c966ed43..00f66635 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,9 +92,10 @@ dependencies { implementation deps.androidx.local_broadcast_manager implementation deps.androidx.material implementation deps.androidx.preference - implementation deps.androidx.swiperefreshlayout implementation deps.androidx.recycler_view implementation deps.androidx.recycler_view_selection + implementation deps.androidx.security + implementation deps.androidx.swiperefreshlayout implementation deps.kotlin.coroutines.android implementation deps.kotlin.coroutines.core diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 03769198..be85558b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,7 +40,16 @@ </intent-filter> </activity> - <activity android:name=".git.GitActivity" /> + <activity android:name=".git.GitOperationActivity" + android:theme="@style/NoBackgroundTheme" /> + + <activity android:name=".git.GitServerConfigActivity" + android:windowSoftInputMode="adjustResize" + android:label="@string/title_activity_git_clone" /> + + <activity android:name=".git.GitConfigActivity" + android:windowSoftInputMode="adjustResize" + android:label="@string/title_activity_git_config" /> <activity android:name=".UserPreference" diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt index e4551f1c..fd7bdf83 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt @@ -23,7 +23,8 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding -import com.zeapo.pwdstore.git.GitActivity +import com.zeapo.pwdstore.git.BaseGitActivity +import com.zeapo.pwdstore.git.GitOperationActivity import com.zeapo.pwdstore.ui.OnOffItemAnimator import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter import com.zeapo.pwdstore.utils.PasswordItem @@ -77,9 +78,9 @@ class PasswordFragment : Fragment() { .show() swipeRefresher.isRefreshing = false } else { - val intent = Intent(context, GitActivity::class.java) - intent.putExtra("Operation", GitActivity.REQUEST_SYNC) - startActivityForResult(intent, GitActivity.REQUEST_SYNC) + val intent = Intent(context, GitOperationActivity::class.java) + intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_SYNC) + startActivityForResult(intent, BaseGitActivity.REQUEST_SYNC) } } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt index dc09eefe..90b2e631 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt @@ -38,9 +38,11 @@ import com.google.android.material.snackbar.Snackbar import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher import com.zeapo.pwdstore.crypto.PgpActivity import com.zeapo.pwdstore.crypto.PgpActivity.Companion.getLongName -import com.zeapo.pwdstore.git.GitActivity +import com.zeapo.pwdstore.git.BaseGitActivity import com.zeapo.pwdstore.git.GitAsyncTask import com.zeapo.pwdstore.git.GitOperation +import com.zeapo.pwdstore.git.GitOperationActivity +import com.zeapo.pwdstore.git.GitServerConfigActivity import com.zeapo.pwdstore.ui.dialogs.FolderCreationDialogFragment import com.zeapo.pwdstore.utils.PasswordItem import com.zeapo.pwdstore.utils.PasswordRepository @@ -249,9 +251,9 @@ class PasswordStore : AppCompatActivity() { initBefore.show() return false } - intent = Intent(this, GitActivity::class.java) - intent.putExtra("Operation", GitActivity.REQUEST_PUSH) - startActivityForResult(intent, GitActivity.REQUEST_PUSH) + intent = Intent(this, GitOperationActivity::class.java) + intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_PUSH) + startActivityForResult(intent, BaseGitActivity.REQUEST_PUSH) return true } R.id.git_pull -> { @@ -259,9 +261,9 @@ class PasswordStore : AppCompatActivity() { initBefore.show() return false } - intent = Intent(this, GitActivity::class.java) - intent.putExtra("Operation", GitActivity.REQUEST_PULL) - startActivityForResult(intent, GitActivity.REQUEST_PULL) + intent = Intent(this, GitOperationActivity::class.java) + intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_PULL) + startActivityForResult(intent, BaseGitActivity.REQUEST_PULL) return true } R.id.git_sync -> { @@ -269,9 +271,9 @@ class PasswordStore : AppCompatActivity() { initBefore.show() return false } - intent = Intent(this, GitActivity::class.java) - intent.putExtra("Operation", GitActivity.REQUEST_SYNC) - startActivityForResult(intent, GitActivity.REQUEST_SYNC) + intent = Intent(this, GitOperationActivity::class.java) + intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_SYNC) + startActivityForResult(intent, BaseGitActivity.REQUEST_SYNC) return true } R.id.refresh -> { @@ -353,7 +355,7 @@ class PasswordStore : AppCompatActivity() { .setMessage(this.resources.getString(R.string.key_dialog_text)) .setPositiveButton(this.resources.getString(R.string.dialog_positive)) { _, _ -> val intent = Intent(activity, UserPreference::class.java) - startActivityForResult(intent, GitActivity.REQUEST_INIT) + startActivityForResult(intent, BaseGitActivity.REQUEST_INIT) } .setNegativeButton(this.resources.getString(R.string.dialog_negative), null) .show() @@ -550,7 +552,7 @@ class PasswordStore : AppCompatActivity() { fileLocations.add(file.absolutePath) } intent.putExtra("Files", fileLocations) - intent.putExtra("Operation", "SELECTFOLDER") + intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, "SELECTFOLDER") startActivityForResult(intent, REQUEST_CODE_SELECT_FOLDER) } @@ -586,7 +588,7 @@ class PasswordStore : AppCompatActivity() { if (resultCode == Activity.RESULT_OK) { when (requestCode) { // if we get here with a RESULT_OK then it's probably OK :) - GitActivity.REQUEST_CLONE -> settings.edit().putBoolean("repository_initialized", true).apply() + BaseGitActivity.REQUEST_CLONE -> settings.edit().putBoolean("repository_initialized", true).apply() // if went from decrypt->edit and user saved changes or HOTP counter was // incremented, we need to commitChange REQUEST_CODE_DECRYPT_AND_VERIFY -> { @@ -620,8 +622,8 @@ class PasswordStore : AppCompatActivity() { data!!.extras!!.getString("LONG_NAME"))) refreshPasswordList() } - GitActivity.REQUEST_INIT, NEW_REPO_BUTTON -> initializeRepositoryInfo() - GitActivity.REQUEST_SYNC, GitActivity.REQUEST_PULL -> resetPasswordList() + BaseGitActivity.REQUEST_INIT, NEW_REPO_BUTTON -> initializeRepositoryInfo() + BaseGitActivity.REQUEST_SYNC, BaseGitActivity.REQUEST_PULL -> resetPasswordList() HOME -> checkLocalRepository() // duplicate code CLONE_REPO_BUTTON -> { @@ -639,9 +641,9 @@ class PasswordStore : AppCompatActivity() { return // if not empty, just show me the passwords! } } - val intent = Intent(activity, GitActivity::class.java) - intent.putExtra("Operation", GitActivity.REQUEST_CLONE) - startActivityForResult(intent, GitActivity.REQUEST_CLONE) + val intent = Intent(activity, GitOperationActivity::class.java) + intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE) + startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE) } REQUEST_CODE_SELECT_FOLDER -> { Timber.tag(TAG) @@ -722,10 +724,9 @@ class PasswordStore : AppCompatActivity() { when (operation) { NEW_REPO_BUTTON -> initializeRepositoryInfo() CLONE_REPO_BUTTON -> { - initialize(this@PasswordStore) - val intent = Intent(activity, GitActivity::class.java) - intent.putExtra("Operation", GitActivity.REQUEST_CLONE) - startActivityForResult(intent, GitActivity.REQUEST_CLONE) + val intent = Intent(activity, GitServerConfigActivity::class.java) + intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE) + startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE) } } } @@ -744,10 +745,9 @@ class PasswordStore : AppCompatActivity() { when (operation) { NEW_REPO_BUTTON -> initializeRepositoryInfo() CLONE_REPO_BUTTON -> { - initialize(this@PasswordStore) - val intent = Intent(activity, GitActivity::class.java) - intent.putExtra("Operation", GitActivity.REQUEST_CLONE) - startActivityForResult(intent, GitActivity.REQUEST_CLONE) + val intent = Intent(activity, GitServerConfigActivity::class.java) + intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE) + startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE) } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 716a9192..eabc35f4 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -37,7 +37,8 @@ import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel import com.zeapo.pwdstore.crypto.PgpActivity -import com.zeapo.pwdstore.git.GitActivity +import com.zeapo.pwdstore.git.GitConfigActivity +import com.zeapo.pwdstore.git.GitServerConfigActivity import com.zeapo.pwdstore.pwgenxkpwd.XkpwdDictionary import com.zeapo.pwdstore.sshkeygen.ShowSshKeyFragment import com.zeapo.pwdstore.sshkeygen.SshKeyGenActivity @@ -45,6 +46,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.auth.AuthenticationResult import com.zeapo.pwdstore.utils.auth.Authenticator import com.zeapo.pwdstore.utils.autofillManager +import com.zeapo.pwdstore.utils.getEncryptedPrefs import java.io.File import java.io.IOException import java.time.LocalDateTime @@ -73,6 +75,7 @@ class UserPreference : AppCompatActivity() { callingActivity = requireActivity() as UserPreference val context = requireContext() val sharedPreferences = preferenceManager.sharedPreferences + val encryptedPreferences = requireActivity().applicationContext.getEncryptedPrefs("git_operation") addPreferencesFromResource(R.xml.preference) @@ -121,7 +124,7 @@ class UserPreference : AppCompatActivity() { selectExternalGitRepositoryPreference?.summary = sharedPreferences.getString("git_external_repo", getString(R.string.no_repo_selected)) viewSshKeyPreference?.isVisible = sharedPreferences.getBoolean("use_generated_key", false) deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean("git_external", false) - sshClearPassphrasePreference?.isVisible = sharedPreferences.getString("ssh_key_passphrase", null)?.isNotEmpty() + sshClearPassphrasePreference?.isVisible = encryptedPreferences.getString("ssh_key_local_passphrase", null)?.isNotEmpty() ?: false clearHotpIncrementPreference?.isVisible = sharedPreferences.getBoolean("hotp_remember_check", false) clearAfterCopyPreference?.isVisible = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0 @@ -172,7 +175,7 @@ class UserPreference : AppCompatActivity() { } sshClearPassphrasePreference?.onPreferenceClickListener = ClickListener { - sharedPreferences.edit().putString("ssh_key_passphrase", null).apply() + encryptedPreferences.edit().putString("ssh_key_local_passphrase", null).apply() it.isVisible = false true } @@ -190,16 +193,12 @@ class UserPreference : AppCompatActivity() { } gitServerPreference?.onPreferenceClickListener = ClickListener { - val intent = Intent(callingActivity, GitActivity::class.java) - intent.putExtra("Operation", GitActivity.EDIT_SERVER) - startActivityForResult(intent, EDIT_GIT_INFO) + startActivity(Intent(callingActivity, GitServerConfigActivity::class.java)) true } gitConfigPreference?.onPreferenceClickListener = ClickListener { - val intent = Intent(callingActivity, GitActivity::class.java) - intent.putExtra("Operation", GitActivity.EDIT_GIT_CONFIG) - startActivityForResult(intent, EDIT_GIT_CONFIG) + startActivity(Intent(callingActivity, GitConfigActivity::class.java)) true } 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" + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt deleted file mode 100644 index 6341fbe1..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt +++ /dev/null @@ -1,670 +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.Context -import android.content.Intent -import android.content.SharedPreferences -import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.Spinner -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.AppCompatTextView -import androidx.preference.PreferenceManager -import com.google.android.material.button.MaterialButton -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.textfield.TextInputEditText -import com.zeapo.pwdstore.R -import com.zeapo.pwdstore.UserPreference -import com.zeapo.pwdstore.git.config.SshApiSessionFactory -import com.zeapo.pwdstore.utils.PasswordRepository -import java.io.File -import java.io.IOException -import java.util.regex.Pattern -import org.apache.commons.io.FileUtils -import org.eclipse.jgit.lib.Constants -import timber.log.Timber - -open class GitActivity : AppCompatActivity() { - private lateinit var context: Context - private lateinit var settings: SharedPreferences - private lateinit var protocol: String - private lateinit var connectionMode: String - private lateinit var hostname: String - private var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null - private var identity: SshApiSessionFactory.ApiIdentity? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - context = requireNotNull(this) - - settings = PreferenceManager.getDefaultSharedPreferences(this) - - protocol = settings.getString("git_remote_protocol", null) ?: "ssh://" - connectionMode = settings.getString("git_remote_auth", null) ?: "ssh-key" - hostname = settings.getString("git_remote_location", null) ?: "" - val operationCode = intent.extras!!.getInt("Operation") - - supportActionBar!!.setDisplayHomeAsUpEnabled(true) - - when (operationCode) { - REQUEST_CLONE, EDIT_SERVER -> { - setContentView(R.layout.activity_git_clone) - setTitle(R.string.title_activity_git_clone) - - val protcolSpinner = findViewById<Spinner>(R.id.clone_protocol) - val connectionModeSpinner = findViewById<Spinner>(R.id.connection_mode) - - // init the spinner for connection modes - val connectionModeAdapter = ArrayAdapter.createFromResource(this, - R.array.connection_modes, android.R.layout.simple_spinner_item) - connectionModeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - connectionModeSpinner.adapter = connectionModeAdapter - connectionModeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) { - val selection = (findViewById<View>(R.id.connection_mode) as Spinner).selectedItem.toString() - connectionMode = selection - settings.edit().putString("git_remote_auth", selection).apply() - } - - override fun onNothingSelected(adapterView: AdapterView<*>) { - } - } - - // init the spinner for protocols - val protocolAdapter = ArrayAdapter.createFromResource(this, - R.array.clone_protocols, android.R.layout.simple_spinner_item) - protocolAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - protcolSpinner.adapter = protocolAdapter - protcolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) { - protocol = (findViewById<View>(R.id.clone_protocol) as Spinner).selectedItem.toString() - if (protocol == "ssh://") { - - // select ssh-key auth mode as default and enable the spinner in case it was disabled - connectionModeSpinner.setSelection(0) - connectionModeSpinner.isEnabled = true - - // however, if we have some saved that, that's more important! - when { - connectionMode.equals("ssh-key", ignoreCase = true) -> connectionModeSpinner.setSelection(0) - connectionMode.equals("OpenKeychain", ignoreCase = true) -> connectionModeSpinner.setSelection(2) - else -> connectionModeSpinner.setSelection(1) - } - } else { - // select user/pwd auth-mode and disable the spinner - connectionModeSpinner.setSelection(1) - connectionModeSpinner.isEnabled = false - } - - updateURI() - } - - override fun onNothingSelected(adapterView: AdapterView<*>) { - } - } - - if (protocol == "ssh://") { - protcolSpinner.setSelection(0) - } else { - protcolSpinner.setSelection(1) - } - - // init the server information - val serverUrl = findViewById<TextInputEditText>(R.id.server_url) - val serverPort = findViewById<TextInputEditText>(R.id.server_port) - val serverPath = findViewById<TextInputEditText>(R.id.server_path) - val serverUser = findViewById<TextInputEditText>(R.id.server_user) - val serverUri = findViewById<TextInputEditText>(R.id.clone_uri) - - serverUrl.setText(settings.getString("git_remote_server", "")) - serverPort.setText(settings.getString("git_remote_port", "")) - serverUser.setText(settings.getString("git_remote_username", "")) - serverPath.setText(settings.getString("git_remote_location", "")) - - serverUrl.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {} - - override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) { - if (serverUrl.isFocused) - updateURI() - } - - override fun afterTextChanged(editable: Editable) {} - }) - serverPort.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {} - - override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) { - if (serverPort.isFocused) - updateURI() - } - - override fun afterTextChanged(editable: Editable) {} - }) - serverUser.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {} - - override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) { - if (serverUser.isFocused) - updateURI() - } - - override fun afterTextChanged(editable: Editable) {} - }) - serverPath.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {} - - override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) { - if (serverPath.isFocused) - updateURI() - } - - override fun afterTextChanged(editable: Editable) {} - }) - - serverUri.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {} - - override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) { - if (serverUri.isFocused) - splitURI() - } - - override fun afterTextChanged(editable: Editable) {} - }) - - if (operationCode == EDIT_SERVER) { - findViewById<View>(R.id.clone_button).visibility = View.INVISIBLE - findViewById<View>(R.id.save_button).visibility = View.VISIBLE - } else { - findViewById<View>(R.id.clone_button).visibility = View.VISIBLE - findViewById<View>(R.id.save_button).visibility = View.INVISIBLE - } - - updateURI() - } - EDIT_GIT_CONFIG -> { - setContentView(R.layout.activity_git_config) - setTitle(R.string.title_activity_git_config) - - showGitConfig() - } - REQUEST_PULL -> syncRepository(REQUEST_PULL) - - REQUEST_PUSH -> syncRepository(REQUEST_PUSH) - - REQUEST_SYNC -> syncRepository(REQUEST_SYNC) - } - } - - /** - * Fills in the server_uri field with the information coming from other fields - */ - private fun updateURI() { - val uri = findViewById<TextInputEditText>(R.id.clone_uri) - val serverUrl = findViewById<TextInputEditText>(R.id.server_url) - val serverPort = findViewById<TextInputEditText>(R.id.server_port) - val serverPath = findViewById<TextInputEditText>(R.id.server_path) - val serverUser = findViewById<TextInputEditText>(R.id.server_user) - - if (uri != null) { - when (protocol) { - "ssh://" -> { - var hostname = (serverUser.text.toString() + - "@" + - serverUrl.text.toString().trim { it <= ' ' } + - ":") - if (serverPort.text.toString() == "22") { - hostname += serverPath.text.toString() - - findViewById<View>(R.id.warn_url).visibility = View.GONE - } else { - val warnUrl = findViewById<AppCompatTextView>(R.id.warn_url) - if (!serverPath.text.toString().matches("/.*".toRegex()) && serverPort.text.toString().isNotEmpty()) { - warnUrl.setText(R.string.warn_malformed_url_port) - warnUrl.visibility = View.VISIBLE - } else { - warnUrl.visibility = View.GONE - } - hostname += serverPort.text.toString() + serverPath.text.toString() - } - - if (hostname != "@:") uri.setText(hostname) - } - "https://" -> { - val hostname = StringBuilder() - hostname.append(serverUrl.text.toString().trim { it <= ' ' }) - - if (serverPort.text.toString() == "443") { - hostname.append(serverPath.text.toString()) - - findViewById<View>(R.id.warn_url).visibility = View.GONE - } else { - hostname.append("/") - hostname.append(serverPort.text.toString()) - .append(serverPath.text.toString()) - } - - if (hostname.toString() != "@/") uri.setText(hostname) - } - else -> { - } - } - } - } - - /** - * Splits the information in server_uri into the other fields - */ - private fun splitURI() { - val serverUri = findViewById<TextInputEditText>(R.id.clone_uri) - val serverUrl = findViewById<TextInputEditText>(R.id.server_url) - val serverPort = findViewById<TextInputEditText>(R.id.server_port) - val serverPath = findViewById<TextInputEditText>(R.id.server_path) - val serverUser = findViewById<TextInputEditText>(R.id.server_user) - - val uri = serverUri.text.toString() - val pattern = Pattern.compile("(.+)@([\\w\\d.]+):([\\d]+)*(.*)") - val matcher = pattern.matcher(uri) - if (matcher.find()) { - val count = matcher.groupCount() - if (count > 1) { - serverUser.setText(matcher.group(1)) - serverUrl.setText(matcher.group(2)) - } - if (count == 4) { - serverPort.setText(matcher.group(3)) - serverPath.setText(matcher.group(4)) - - val warnUrl = findViewById<AppCompatTextView>(R.id.warn_url) - if (!serverPath.text.toString().matches("/.*".toRegex()) && serverPort.text.toString().isNotEmpty()) { - warnUrl.setText(R.string.warn_malformed_url_port) - warnUrl.visibility = View.VISIBLE - } else { - warnUrl.visibility = View.GONE - } - } - } - } - - public override fun onResume() { - super.onResume() - updateURI() - } - - override fun onDestroy() { - // Do not leak the service connection - if (identityBuilder != null) { - identityBuilder!!.close() - identityBuilder = null - } - super.onDestroy() - } - - 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 { - when (item.itemId) { - R.id.user_pref -> try { - val intent = Intent(this, UserPreference::class.java) - startActivity(intent) - return true - } catch (e: Exception) { - println("Exception caught :(") - e.printStackTrace() - } - - android.R.id.home -> { - finish() - return true - } - } - return super.onOptionsItemSelected(item) - } - - /** - * Saves the configuration found in the form - */ - private fun saveConfiguration(): Boolean { - // remember the settings - val editor = settings.edit() - - editor.putString("git_remote_server", (findViewById<View>(R.id.server_url) as TextInputEditText).text.toString()) - editor.putString("git_remote_location", (findViewById<View>(R.id.server_path) as TextInputEditText).text.toString()) - editor.putString("git_remote_username", (findViewById<View>(R.id.server_user) as TextInputEditText).text.toString()) - editor.putString("git_remote_protocol", protocol) - editor.putString("git_remote_auth", connectionMode) - editor.putString("git_remote_port", (findViewById<View>(R.id.server_port) as TextInputEditText).text.toString()) - editor.putString("git_remote_uri", (findViewById<View>(R.id.clone_uri) as TextInputEditText).text.toString()) - - // 'save' hostname variable for use by addRemote() either here or later - // in syncRepository() - hostname = (findViewById<View>(R.id.clone_uri) as TextInputEditText).text.toString() - val port = (findViewById<View>(R.id.server_port) as TextInputEditText).text.toString() - // don't ask the user, take off the protocol that he puts in - hostname = hostname.replaceFirst("^.+://".toRegex(), "") - (findViewById<View>(R.id.clone_uri) as TextInputEditText).setText(hostname) - - if (protocol != "ssh://") { - hostname = protocol + hostname - } else { - // if the port is explicitly given, jgit requires the ssh:// - if (port.isNotEmpty() && port != "22") - hostname = protocol + hostname - - // did he forget the username? - if (!hostname.matches("^.+@.+".toRegex())) { - MaterialAlertDialogBuilder(this) - .setMessage(context.getString(R.string.forget_username_dialog_text)) - .setPositiveButton(context.getString(R.string.dialog_oops), null) - .show() - return false - } - } - if (PasswordRepository.isInitialized && settings.getBoolean("repository_initialized", false)) { - // don't just use the clone_uri text, need to use hostname which has - // had the proper protocol prepended - PasswordRepository.addRemote("origin", hostname, true) - } - - editor.apply() - return true - } - - /** - * Save the repository information to the shared preferences settings - */ - @Suppress("UNUSED_PARAMETER") - fun saveConfiguration(view: View) { - if (!saveConfiguration()) - return - finish() - } - - private fun showGitConfig() { - // init the server information - val username = findViewById<TextInputEditText>(R.id.git_user_name) - val email = findViewById<TextInputEditText>(R.id.git_user_email) - val abort = findViewById<MaterialButton>(R.id.git_abort_rebase) - - username.setText(settings.getString("git_config_user_name", "")) - email.setText(settings.getString("git_config_user_email", "")) - - // git status - val repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory(context)) - if (repo != null) { - val commitHash = findViewById<AppCompatTextView>(R.id.git_commit_hash) - try { - val objectId = repo.resolve(Constants.HEAD) - val ref = repo.getRef("refs/heads/master") - val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED" - commitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head) - - // enable the abort button only if we're rebasing - val isRebasing = repo.repositoryState.isRebasing - abort.isEnabled = isRebasing - abort.alpha = if (isRebasing) 1.0f else 0.5f - } catch (e: Exception) { - // ignore - } - } - } - - private fun saveGitConfigs(): Boolean { - // remember the settings - val editor = settings.edit() - - val email = (findViewById<View>(R.id.git_user_email) as TextInputEditText).text!!.toString() - editor.putString("git_config_user_email", email) - editor.putString("git_config_user_name", (findViewById<View>(R.id.git_user_name) as TextInputEditText).text.toString()) - - if (!email.matches(emailPattern.toRegex())) { - MaterialAlertDialogBuilder(this) - .setMessage(context.getString(R.string.invalid_email_dialog_text)) - .setPositiveButton(context.getString(R.string.dialog_oops), null) - .show() - return false - } - - editor.apply() - return true - } - - @Suppress("UNUSED_PARAMETER") - fun applyGitConfigs(view: View) { - if (!saveGitConfigs()) - return - PasswordRepository.setUserName(settings.getString("git_config_user_name", null) ?: "") - PasswordRepository.setUserEmail(settings.getString("git_config_user_email", null) ?: "") - finish() - } - - @Suppress("UNUSED_PARAMETER") - fun abortRebase(view: View) { - launchGitOperation(BREAK_OUT_OF_DETACHED) - } - - @Suppress("UNUSED_PARAMETER") - fun resetToRemote(view: View) { - launchGitOperation(REQUEST_RESET) - } - - /** - * Clones the repository, the directory exists, deletes it - */ - @Suppress("UNUSED_PARAMETER") - fun cloneRepository(view: View) { - if (PasswordRepository.getRepository(null) == null) { - PasswordRepository.initialize(this) - } - val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(context)) - - if (!saveConfiguration()) - return - - // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder - if (localDir.exists() && localDir.listFiles()!!.isNotEmpty() && - !(localDir.listFiles()!!.size == 1 && localDir.listFiles()!![0].name == ".git")) { - MaterialAlertDialogBuilder(this) - .setTitle(R.string.dialog_delete_title) - .setMessage(resources.getString(R.string.dialog_delete_msg) + " " + localDir.toString()) - .setCancelable(false) - .setPositiveButton(R.string.dialog_delete - ) { dialog, _ -> - try { - FileUtils.deleteDirectory(localDir) - launchGitOperation(REQUEST_CLONE) - } catch (e: IOException) { - // TODO Handle the exception correctly if we are unable to delete the directory... - e.printStackTrace() - MaterialAlertDialogBuilder(this).setMessage(e.message).show() - } - - dialog.cancel() - } - .setNegativeButton(R.string.dialog_do_not_delete - ) { dialog, _ -> dialog.cancel() } - .show() - } else { - try { - // Silently delete & replace the lone .git folder if it exists - if (localDir.exists() && localDir.listFiles()!!.size == 1 && localDir.listFiles()!![0].name == ".git") { - try { - FileUtils.deleteDirectory(localDir) - } catch (e: IOException) { - e.printStackTrace() - MaterialAlertDialogBuilder(this).setMessage(e.message).show() - } - } - } catch (e: Exception) { - // This is what happens when jgit fails :( - // TODO Handle the diffent cases of exceptions - e.printStackTrace() - MaterialAlertDialogBuilder(this).setMessage(e.message).show() - } - - launchGitOperation(REQUEST_CLONE) - } - } - - /** - * 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 fun syncRepository(operation: Int) { - if (settings.getString("git_remote_username", "")!!.isEmpty() || - settings.getString("git_remote_server", "")!!.isEmpty() || - settings.getString("git_remote_location", "")!!.isEmpty()) - MaterialAlertDialogBuilder(this) - .setMessage(context.getString(R.string.set_information_dialog_text)) - .setPositiveButton(context.getString(R.string.dialog_positive)) { _, _ -> - val intent = Intent(context, UserPreference::class.java) - startActivityForResult(intent, REQUEST_PULL) - } - .setNegativeButton(context.getString(R.string.dialog_negative)) { _, _ -> - // do nothing :( - setResult(AppCompatActivity.RESULT_OK) - finish() - } - .show() - else { - // check that the remote origin is here, else add it - PasswordRepository.addRemote("origin", hostname, false) - launchGitOperation(operation) - } - } - - /** - * 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 - */ - private fun launchGitOperation(operation: Int) { - val op: GitOperation - val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(context)) - - 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.equals("OpenKeychain", ignoreCase = true) && 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 - } - - when (operation) { - REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> op = CloneOperation(localDir, this).setCommand(hostname) - - REQUEST_PULL -> op = PullOperation(localDir, this).setCommand() - - REQUEST_PUSH -> op = PushOperation(localDir, this).setCommand() - - REQUEST_SYNC -> op = SyncOperation(localDir, this).setCommands() - - BREAK_OUT_OF_DETACHED -> op = BreakOutOfDetached(localDir, this).setCommands() - - REQUEST_RESET -> op = ResetToRemoteOperation(localDir, this).setCommands() - - SshApiSessionFactory.POST_SIGNATURE -> return - - else -> { - Timber.tag(TAG).e("Operation not recognized : $operation") - setResult(AppCompatActivity.RESULT_CANCELED) - finish() - return - } - } - - op.executeAfterAuthentication(connectionMode, - settings.getString("git_remote_username", "git")!!, - 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 == AppCompatActivity.RESULT_CANCELED) { - setResult(AppCompatActivity.RESULT_CANCELED) - finish() - } else if (resultCode == AppCompatActivity.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_PULL = 101 - const val REQUEST_PUSH = 102 - const val REQUEST_CLONE = 103 - const val REQUEST_INIT = 104 - const val EDIT_SERVER = 105 - const val REQUEST_SYNC = 106 - - @Suppress("Unused") - const val REQUEST_CREATE = 107 - const val EDIT_GIT_CONFIG = 108 - const val BREAK_OUT_OF_DETACHED = 109 - const val REQUEST_RESET = 110 - private const val TAG = "GitAct" - private const val emailPattern = "^[^@]+@[^@]+$" - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt new file mode 100644 index 00000000..8a05ca40 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt @@ -0,0 +1,62 @@ +/* + * 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.os.Bundle +import android.util.Patterns +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.databinding.ActivityGitConfigBinding +import com.zeapo.pwdstore.utils.PasswordRepository +import org.eclipse.jgit.lib.Constants + +class GitConfigActivity : BaseGitActivity() { + + private lateinit var binding: ActivityGitConfigBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityGitConfigBinding.inflate(layoutInflater) + setContentView(binding.root) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + binding.gitUserName.setText(username) + binding.gitUserEmail.setText(email) + val repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory(this)) + if (repo != null) { + try { + val objectId = repo.resolve(Constants.HEAD) + val ref = repo.getRef("refs/heads/master") + val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED" + binding.gitCommitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head) + + // enable the abort button only if we're rebasing + val isRebasing = repo.repositoryState.isRebasing + binding.gitAbortRebase.isEnabled = isRebasing + binding.gitAbortRebase.alpha = if (isRebasing) 1.0f else 0.5f + } catch (ignored: Exception) { + } + } + binding.gitAbortRebase.setOnClickListener { launchGitOperation(BREAK_OUT_OF_DETACHED) } + binding.gitResetToRemote.setOnClickListener { launchGitOperation(REQUEST_RESET) } + binding.saveButton.setOnClickListener { + val email = binding.gitUserEmail.text.toString().trim() + val name = binding.gitUserName.text.toString().trim() + if (!email.matches(Patterns.EMAIL_ADDRESS.toRegex())) { + MaterialAlertDialogBuilder(this) + .setMessage(getString(R.string.invalid_email_dialog_text)) + .setPositiveButton(getString(R.string.dialog_ok), null) + .show() + } else { + val editor = settings.edit() + editor.putString("git_config_user_email", email) + editor.putString("git_config_user_name", name) + PasswordRepository.setUserName(name) + PasswordRepository.setUserEmail(email) + editor.apply() + } + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt index ea9ee654..78a9ca69 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt @@ -7,23 +7,24 @@ package com.zeapo.pwdstore.git import android.annotation.SuppressLint import android.app.Activity import android.content.Intent -import android.text.InputType import android.view.LayoutInflater -import android.view.View -import android.widget.CheckBox -import android.widget.EditText -import android.widget.LinearLayout +import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText import com.jcraft.jsch.JSch import com.jcraft.jsch.JSchException import com.jcraft.jsch.KeyPair import com.zeapo.pwdstore.R import com.zeapo.pwdstore.UserPreference +import com.zeapo.pwdstore.git.config.ConnectionMode import com.zeapo.pwdstore.git.config.GitConfigSessionFactory import com.zeapo.pwdstore.git.config.SshApiSessionFactory import com.zeapo.pwdstore.git.config.SshConfigSessionFactory import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.requestInputFocusOnView import java.io.File import org.eclipse.jgit.api.GitCommand import org.eclipse.jgit.lib.Repository @@ -96,7 +97,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit * @param identity the api identity to use for auth in OpenKeychain connection mode */ fun executeAfterAuthentication( - connectionMode: String, + connectionMode: ConnectionMode, username: String, sshKey: File?, identity: SshApiSessionFactory.ApiIdentity? @@ -114,15 +115,17 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit * @param showError show the passphrase edit text in red */ private fun executeAfterAuthentication( - connectionMode: String, + connectionMode: ConnectionMode, username: String, sshKey: File?, identity: SshApiSessionFactory.ApiIdentity?, showError: Boolean ) { - if (connectionMode.equals("ssh-key", ignoreCase = true)) { - if (sshKey == null || !sshKey.exists()) { - MaterialAlertDialogBuilder(callingActivity) + val encryptedSettings = callingActivity.applicationContext.getEncryptedPrefs("git_operation") + when (connectionMode) { + ConnectionMode.SshKey -> { + if (sshKey == null || !sshKey.exists()) { + MaterialAlertDialogBuilder(callingActivity) .setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text)) .setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title)) .setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ -> @@ -152,82 +155,100 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit // Finish the blank GitActivity so user doesn't have to press back callingActivity.finish() }.show() - } else { - val layoutInflater = LayoutInflater.from(callingActivity.applicationContext) - @SuppressLint("InflateParams") val dialogView = layoutInflater.inflate(R.layout.git_passphrase_layout, null) - val passphrase = dialogView.findViewById<EditText>(R.id.sshkey_passphrase) - val settings = PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext) - val sshKeyPassphrase = settings.getString("ssh_key_passphrase", null) - if (showError) { - passphrase.error = "Wrong passphrase" - } - val jsch = JSch() - try { - val keyPair = KeyPair.load(jsch, callingActivity.filesDir.toString() + "/.ssh_key") + } else { + val layoutInflater = LayoutInflater.from(callingActivity) + @SuppressLint("InflateParams") val dialogView = layoutInflater.inflate(R.layout.git_passphrase_layout, null) + val passphrase = dialogView.findViewById<TextInputEditText>(R.id.git_auth_passphrase) + val sshKeyPassphrase = encryptedSettings.getString("ssh_key_local_passphrase", null) + if (showError) { + passphrase.error = callingActivity.resources.getString(R.string.git_operation_wrong_passphrase) + } + val jsch = JSch() + try { + val keyPair = KeyPair.load(jsch, callingActivity.filesDir.toString() + "/.ssh_key") - if (keyPair.isEncrypted) { - if (sshKeyPassphrase != null && sshKeyPassphrase.isNotEmpty()) { - if (keyPair.decrypt(sshKeyPassphrase)) { - // Authenticate using the ssh-key and then execute the command - setAuthentication(sshKey, username, sshKeyPassphrase).execute() + if (keyPair.isEncrypted) { + if (sshKeyPassphrase != null && sshKeyPassphrase.isNotEmpty()) { + if (keyPair.decrypt(sshKeyPassphrase)) { + // Authenticate using the ssh-key and then execute the command + setAuthentication(sshKey, username, sshKeyPassphrase).execute() + } else { + // call back the method + executeAfterAuthentication(connectionMode, username, sshKey, identity, true) + } } else { - // call back the method - executeAfterAuthentication(connectionMode, username, sshKey, identity, true) - } - } else { - MaterialAlertDialogBuilder(callingActivity) + val dialog = MaterialAlertDialogBuilder(callingActivity) .setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title)) .setMessage(callingActivity.resources.getString(R.string.passphrase_dialog_text)) .setView(dialogView) .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> if (keyPair.decrypt(passphrase.text.toString())) { - val rememberPassphrase = (dialogView.findViewById<View>(R.id.sshkey_remember_passphrase) as CheckBox).isChecked + val rememberPassphrase = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_passphrase).isChecked if (rememberPassphrase) { - settings.edit().putString("ssh_key_passphrase", passphrase.text.toString()).apply() + encryptedSettings.edit().putString("ssh_key_local_passphrase", passphrase.text.toString()).apply() } // Authenticate using the ssh-key and then execute the command setAuthentication(sshKey, username, passphrase.text.toString()).execute() } else { - settings.edit().putString("ssh_key_passphrase", null).apply() + encryptedSettings.edit().putString("ssh_key_local_passphrase", null).apply() // call back the method executeAfterAuthentication(connectionMode, username, sshKey, identity, true) } - }.setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> - // Do nothing. - }.show() + } + .setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> + callingActivity.finish() + } + .setOnCancelListener { callingActivity.finish() } + .create() + dialog.requestInputFocusOnView<TextInputEditText>(R.id.git_auth_passphrase) + dialog.show() + } + } else { + setAuthentication(sshKey, username, "").execute() } - } else { - setAuthentication(sshKey, username, "").execute() - } - } catch (e: JSchException) { - e.printStackTrace() - MaterialAlertDialogBuilder(callingActivity) - .setTitle("Unable to open the ssh-key") - .setMessage("Please check that it was imported.") - .setPositiveButton("Ok") { _, _ -> callingActivity.finish() } + } catch (e: JSchException) { + e.printStackTrace() + MaterialAlertDialogBuilder(callingActivity) + .setTitle(callingActivity.resources.getString(R.string.git_operation_unable_to_open_ssh_key_title)) + .setMessage(callingActivity.resources.getString(R.string.git_operation_unable_to_open_ssh_key_message)) + .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> + callingActivity.finish() + } .show() + } + } + } + ConnectionMode.OpenKeychain -> { + setAuthentication(username, identity).execute() + } + ConnectionMode.Password -> { + @SuppressLint("InflateParams") val dialogView = callingActivity.layoutInflater.inflate(R.layout.git_passphrase_layout, null) + val passwordView = dialogView.findViewById<TextInputEditText>(R.id.git_auth_passphrase) + val password = encryptedSettings.getString("https_password", null) + if (password != null && password.isNotEmpty()) { + setAuthentication(username, password).execute() + } else { + val dialog = MaterialAlertDialogBuilder(callingActivity) + .setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title)) + .setMessage(callingActivity.resources.getString(R.string.password_dialog_text)) + .setView(dialogView) + .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> + val rememberPassphrase = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_passphrase).isChecked + if (rememberPassphrase) { + encryptedSettings.edit().putString("https_password", passwordView.text.toString()).apply() + } + // authenticate using the user/pwd and then execute the command + setAuthentication(username, passwordView.text.toString()).execute() + } + .setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> + callingActivity.finish() + } + .setOnCancelListener { callingActivity.finish() } + .create() + dialog.requestInputFocusOnView<TextInputEditText>(R.id.git_auth_passphrase) + dialog.show() } } - } else if (connectionMode.equals("OpenKeychain", ignoreCase = true)) { - setAuthentication(username, identity).execute() - } else { - val password = EditText(callingActivity) - password.hint = "Password" - password.width = LinearLayout.LayoutParams.MATCH_PARENT - password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - - MaterialAlertDialogBuilder(callingActivity) - .setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title)) - .setMessage(callingActivity.resources.getString(R.string.password_dialog_text)) - .setView(password) - .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> - // authenticate using the user/pwd and then execute the command - setAuthentication(username, password.text.toString()).execute() - } - .setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> - callingActivity.finish() - } - .show() } } @@ -235,10 +256,17 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit * Action to execute on error */ open fun onError(errorMessage: String) { + // Clear various auth related fields on failure if (SshSessionFactory.getInstance() is SshApiSessionFactory) { - // Clear stored key id from settings on auth failure PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext) - .edit().putString("ssh_openkeystore_keyid", null).apply() + .edit { putString("ssh_openkeystore_keyid", null) } + callingActivity.applicationContext + .getEncryptedPrefs("git_operation") + .edit { remove("ssh_key_local_passphrase") } + } else if (SshSessionFactory.getInstance() is GitConfigSessionFactory) { + callingActivity.applicationContext + .getEncryptedPrefs("git_operation") + .edit { remove("https_password") } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt new file mode 100644 index 00000000..bf7d5acc --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt @@ -0,0 +1,72 @@ +/* + * 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 com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.UserPreference +import com.zeapo.pwdstore.utils.PasswordRepository + +open class GitOperationActivity : BaseGitActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + when (intent.extras?.getInt(REQUEST_ARG_OP)) { + REQUEST_PULL -> syncRepository(REQUEST_PULL) + REQUEST_PUSH -> syncRepository(REQUEST_PUSH) + REQUEST_SYNC -> syncRepository(REQUEST_SYNC) + } + } + + 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 fun syncRepository(operation: Int) { + if (serverUser.isEmpty() || serverHostname.isEmpty() || url.isEmpty()) + 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", url, true) + launchGitOperation(operation) + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt new file mode 100644 index 00000000..a10f3460 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt @@ -0,0 +1,185 @@ +/* + * 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.os.Bundle +import android.os.Handler +import androidx.core.content.edit +import androidx.core.os.postDelayed +import androidx.core.widget.doOnTextChanged +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.databinding.ActivityGitCloneBinding +import com.zeapo.pwdstore.git.config.ConnectionMode +import com.zeapo.pwdstore.git.config.Protocol +import com.zeapo.pwdstore.utils.PasswordRepository +import java.io.IOException + +/** + * Activity that encompasses both the initial clone as well as editing the server config for future + * changes. + */ +class GitServerConfigActivity : BaseGitActivity() { + + lateinit var binding: ActivityGitCloneBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityGitCloneBinding.inflate(layoutInflater) + val isClone = intent?.extras?.getInt(REQUEST_ARG_OP) ?: -1 == REQUEST_CLONE + if (isClone) { + binding.saveButton.text = getString(R.string.clone_button) + } + setContentView(binding.root) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + val protocolIdToCheck = when (protocol) { + Protocol.Ssh -> R.id.clone_protocol_ssh + Protocol.Https -> R.id.clone_protocol_https + } + binding.cloneProtocolGroup.check(protocolIdToCheck) + binding.cloneProtocolGroup.addOnButtonCheckedListener { _, checkedId, checked -> + if (checked) { + when (checkedId) { + R.id.clone_protocol_https -> protocol = Protocol.Https + R.id.clone_protocol_ssh -> protocol = Protocol.Ssh + } + updateConnectionModeToggleGroup() + } + } + + val connectionModeIdToCheck = when (connectionMode) { + ConnectionMode.SshKey -> R.id.connection_mode_ssh_key + ConnectionMode.Password -> R.id.connection_mode_password + ConnectionMode.OpenKeychain -> R.id.connection_mode_open_keychain + } + binding.connectionModeGroup.check(connectionModeIdToCheck) + binding.connectionModeGroup.addOnButtonCheckedListener { _, checkedId, checked -> + if (checked) { + when (checkedId) { + R.id.connection_mode_ssh_key -> connectionMode = ConnectionMode.SshKey + R.id.connection_mode_open_keychain -> connectionMode = ConnectionMode.OpenKeychain + R.id.connection_mode_password -> connectionMode = ConnectionMode.Password + } + } + } + updateConnectionModeToggleGroup() + + binding.serverUrl.apply { + setText(serverHostname) + doOnTextChanged { text, _, _, _ -> + serverHostname = text.toString().trim() + } + } + + binding.serverPort.apply { + setText(serverPort) + doOnTextChanged { text, _, _, _ -> + serverPort = text.toString().trim() + } + } + + binding.serverUser.apply { + setText(serverUser) + doOnTextChanged { text, _, _, _ -> + serverUser = text.toString().trim() + } + } + + binding.serverPath.apply { + setText(serverPath) + doOnTextChanged { text, _, _, _ -> + serverPath = text.toString().trim() + } + } + + binding.saveButton.setOnClickListener { + if (isClone && PasswordRepository.getRepository(null) == null) + PasswordRepository.initialize(this) + if (updateUrl()) { + settings.edit { + putString("git_remote_protocol", protocol.pref) + putString("git_remote_auth", connectionMode.pref) + putString("git_remote_server", serverHostname) + putString("git_remote_port", serverPort) + putString("git_remote_username", serverUser) + putString("git_remote_location", serverPath) + } + if (!isClone) { + Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show() + Handler().postDelayed(500) { finish() } + } else + cloneRepository() + } else { + Snackbar.make(binding.root, getString(R.string.git_server_config_save_failure), Snackbar.LENGTH_LONG).show() + } + } + } + + private fun updateConnectionModeToggleGroup() { + if (protocol == Protocol.Ssh) { + binding.connectionModeSshKey.isEnabled = true + binding.connectionModeOpenKeychain.isEnabled = true + } else { + // Reset connection mode to the only one possible via HTTPS: password. + // Important note: This has to happen before disabling the other toggle buttons or they + // won't uncheck. + binding.connectionModeGroup.check(R.id.connection_mode_password) + binding.connectionModeSshKey.isEnabled = false + binding.connectionModeOpenKeychain.isEnabled = false + } + } + + /** + * Clones the repository, the directory exists, deletes it + */ + private fun cloneRepository() { + val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this)) + val localDirFiles = localDir.listFiles() ?: emptyArray() + // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder + if (localDir.exists() && localDirFiles.isNotEmpty() && + !(localDirFiles.size == 1 && localDirFiles[0].name == ".git")) { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.dialog_delete_title) + .setMessage(resources.getString(R.string.dialog_delete_msg) + " " + localDir.toString()) + .setCancelable(false) + .setPositiveButton(R.string.dialog_delete) { dialog, _ -> + try { + localDir.deleteRecursively() + launchGitOperation(REQUEST_CLONE) + } catch (e: IOException) { + // TODO Handle the exception correctly if we are unable to delete the directory... + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } finally { + dialog.cancel() + } + } + .setNegativeButton(R.string.dialog_do_not_delete) { dialog, _ -> + dialog.cancel() + } + .show() + } else { + try { + // Silently delete & replace the lone .git folder if it exists + if (localDir.exists() && localDirFiles.size == 1 && localDirFiles[0].name == ".git") { + try { + localDir.deleteRecursively() + } catch (e: IOException) { + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } + } + } catch (e: Exception) { + // This is what happens when JGit fails :( + // TODO Handle the different cases of exceptions + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } + launchGitOperation(REQUEST_CLONE) + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/ConnectionMode.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/ConnectionMode.kt new file mode 100644 index 00000000..9316e89f --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/ConnectionMode.kt @@ -0,0 +1,19 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git.config + +enum class ConnectionMode(val pref: String) { + SshKey("ssh-key"), + Password("username/password"), + OpenKeychain("OpenKeychain"); + + companion object { + private val map = values().associateBy(ConnectionMode::pref) + fun fromString(type: String?): ConnectionMode { + return map[type ?: return SshKey] + ?: throw IllegalArgumentException("$type is not a valid ConnectionMode") + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/Protocol.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/Protocol.kt new file mode 100644 index 00000000..1909fe85 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/Protocol.kt @@ -0,0 +1,18 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git.config + +enum class Protocol(val pref: String) { + Ssh("ssh://"), + Https("https://"); + + companion object { + private val map = values().associateBy(Protocol::pref) + fun fromString(type: String?): Protocol { + return map[type ?: return Ssh] + ?: throw IllegalArgumentException("$type is not a valid Protocol") + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java index a75ddef8..6b82cac9 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java @@ -18,7 +18,7 @@ import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import com.jcraft.jsch.UserInfo; import com.zeapo.pwdstore.R; -import com.zeapo.pwdstore.git.GitActivity; +import com.zeapo.pwdstore.git.BaseGitActivity; import java.util.List; import java.util.concurrent.CountDownLatch; import org.eclipse.jgit.errors.UnsupportedCredentialItem; @@ -107,7 +107,7 @@ public class SshApiSessionFactory extends GitConfigSessionFactory { private SshAuthenticationApi api; private String keyId, description, alg; private byte[] publicKey; - private GitActivity callingActivity; + private BaseGitActivity callingActivity; private SharedPreferences settings; /** @@ -116,7 +116,7 @@ public class SshApiSessionFactory extends GitConfigSessionFactory { * @param callingActivity Activity that will be used to launch pending intents and that will * receive and handle the results. */ - public IdentityBuilder(GitActivity callingActivity) { + public IdentityBuilder(BaseGitActivity callingActivity) { this.callingActivity = callingActivity; List<String> providers = diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt index 1426ef1e..a5ff0bc1 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt @@ -6,13 +6,13 @@ package com.zeapo.pwdstore.ui.dialogs import android.app.Dialog import android.os.Bundle -import android.view.inputmethod.InputMethodManager import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText import com.zeapo.pwdstore.PasswordStore import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.utils.requestInputFocusOnView import java.io.File class FolderCreationDialogFragment : DialogFragment() { @@ -28,18 +28,7 @@ class FolderCreationDialogFragment : DialogFragment() { dismiss() } val dialog = alertDialogBuilder.create() - dialog.setOnShowListener { - // https://stackoverflow.com/a/13056259/297261 - dialog.findViewById<TextInputEditText>(R.id.folder_name_text)!!.apply { - setOnFocusChangeListener { v, _ -> - v.post { - val imm = activity!!.getSystemService(InputMethodManager::class.java) - imm?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT) - } - } - requestFocus() - } - } + dialog.requestInputFocusOnView<TextInputEditText>(R.id.folder_name_text) return dialog } diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index f7a637c3..6f4ebab1 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -5,10 +5,18 @@ package com.zeapo.pwdstore.utils import android.content.Context +import android.content.SharedPreferences import android.os.Build import android.util.TypedValue +import android.view.View import android.view.autofill.AutofillManager +import android.view.inputmethod.InputMethodManager +import androidx.annotation.IdRes import androidx.annotation.RequiresApi +import androidx.appcompat.app.AlertDialog +import androidx.core.content.getSystemService +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys infix fun Int.hasFlag(flag: Int): Boolean { return this and flag == flag @@ -24,6 +32,37 @@ fun Context.resolveAttribute(attr: Int): Int { return typedValue.data } +fun Context.getEncryptedPrefs(fileName: String): SharedPreferences { + val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC + val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec) + return EncryptedSharedPreferences.create( + fileName, + masterKeyAlias, + this, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) +} + +/** + * Extension function for [AlertDialog] that requests focus for the + * view whose id is [id]. Solution based on a StackOverflow + * answer: https://stackoverflow.com/a/13056259/297261 + */ +fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) { + setOnShowListener { + findViewById<T>(id)?.apply { + setOnFocusChangeListener { v, _ -> + v.post { + context.getSystemService<InputMethodManager>() + ?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT) + } + } + requestFocus() + } + } +} + val Context.autofillManager: AutofillManager? @RequiresApi(Build.VERSION_CODES.O) get() = getSystemService(AutofillManager::class.java) diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt index 1cf8fea0..2cc19ae3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt @@ -95,7 +95,7 @@ open class PasswordRepository protected constructor() { // TODO add multiple remotes support for pull/push @JvmStatic - fun addRemote(name: String, url: String, replace: Boolean?) { + fun addRemote(name: String, url: String, replace: Boolean = false) { val storedConfig = repository!!.config val remotes = storedConfig.getSubsections("remote") @@ -116,7 +116,7 @@ open class PasswordRepository protected constructor() { } catch (e: Exception) { e.printStackTrace() } - } else if (replace!!) { + } else if (replace) { try { val uri = URIish(url) @@ -181,16 +181,6 @@ open class PasswordRepository protected constructor() { } /** - * Gets the password items in the root directory - * - * @return a list of passwords in the root directory - */ - @JvmStatic - fun getPasswords(rootDir: File, sortOrder: PasswordSortOrder): ArrayList<PasswordItem> { - return getPasswords(rootDir, rootDir, sortOrder) - } - - /** * Gets the .gpg files in a directory * * @param path the directory path diff --git a/app/src/main/res/color/toggle_button_selector.xml b/app/src/main/res/color/toggle_button_selector.xml new file mode 100644 index 00000000..cb4de6a7 --- /dev/null +++ b/app/src/main/res/color/toggle_button_selector.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_checked="false" + android:color="#00FFFFFF" /> + <item android:color="@color/button_color" /> +</selector> diff --git a/app/src/main/res/drawable/bottom_line.xml b/app/src/main/res/drawable/bottom_line.xml deleted file mode 100644 index e67c96fa..00000000 --- a/app/src/main/res/drawable/bottom_line.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> - <item> - <shape android:shape="rectangle"> - <solid android:color="?android:attr/textColor"/> - </shape> - </item> - - <item android:bottom="2dp"> - <shape android:shape="rectangle"> - <solid android:color="?android:attr/windowBackground" /> - </shape> - </item> -</layer-list> diff --git a/app/src/main/res/drawable/red_rectangle.xml b/app/src/main/res/drawable/red_rectangle.xml deleted file mode 100644 index bec893f2..00000000 --- a/app/src/main/res/drawable/red_rectangle.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> - <item> - <shape android:shape="rectangle" - android:dither="true"> - <corners android:radius="2dp"/> - <solid android:color="#ccc" /> - - </shape> - </item> - - <item> - <shape android:shape="rectangle" android:dither="true"> - <corners android:radius="2dp" /> - <solid android:color="#FF0000" /> - - <padding android:bottom="8dp" - android:left="8dp" - android:right="8dp" - android:top="8dp" /> - </shape> - </item> -</layer-list> diff --git a/app/src/main/res/layout/activity_git_clone.xml b/app/src/main/res/layout/activity_git_clone.xml index 05d9a557..de9a240e 100644 --- a/app/src/main/res/layout/activity_git_clone.xml +++ b/app/src/main/res/layout/activity_git_clone.xml @@ -1,178 +1,216 @@ -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - xmlns:app="http://schemas.android.com/apk/res-auto" android:padding="@dimen/activity_horizontal_margin" - tools:context="com.zeapo.pwdstore.git.GitActivity" + tools:context="com.zeapo.pwdstore.git.GitOperationActivity" android:background="?android:attr/windowBackground"> - <androidx.appcompat.widget.AppCompatTextView - style="@style/TextAppearance.MaterialComponents.Headline5" - android:id="@+id/server_label" - android:textStyle="bold" - android:textSize="24sp" - android:text="@string/server_name" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_margin="8dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <androidx.appcompat.widget.AppCompatTextView - android:id="@+id/label_server_protocol" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/server_protocol" - android:layout_margin="8dp" - app:layout_constraintTop_toBottomOf="@id/server_label" - app:layout_constraintStart_toStartOf="parent" /> - - <Spinner - android:id="@+id/clone_protocol" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="8dp" - app:layout_constraintTop_toBottomOf="@id/server_label" - app:layout_constraintStart_toEndOf="@id/label_server_protocol" /> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/server_user_layout" - android:hint="@string/server_user" + <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_margin="8dp" - app:layout_constraintTop_toBottomOf="@id/label_server_protocol"> - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/server_user" + android:layout_height="wrap_content"> + + <androidx.appcompat.widget.AppCompatTextView + style="@style/TextAppearance.MaterialComponents.Headline5" + android:id="@+id/server_label" + android:textStyle="bold" + android:textSize="24sp" + android:text="@string/server_name" android:layout_width="match_parent" android:layout_height="wrap_content" - android:inputType="textWebEmailAddress" /> - </com.google.android.material.textfield.TextInputLayout> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/label_server_url" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_margin="8dp" - android:hint="@string/server_url" - app:layout_constraintTop_toBottomOf="@id/server_user_layout" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/label_server_port"> - - <com.google.android.material.textfield.TextInputEditText - android:layout_width="match_parent" + android:layout_margin="8dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.appcompat.widget.AppCompatTextView + style="@style/TextAppearance.MaterialComponents.Headline6" + android:id="@+id/label_server_protocol" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/server_protocol" + android:layout_margin="8dp" + app:layout_constraintTop_toBottomOf="@id/server_label" + app:layout_constraintStart_toStartOf="parent" /> + + <Spinner + android:id="@+id/clone_protocol" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/server_label" + app:layout_constraintStart_toEndOf="@id/label_server_protocol" /> + + <com.google.android.material.button.MaterialButtonToggleGroup + style="@style/TextAppearance.MaterialComponents.Headline1" + android:id="@+id/clone_protocol_group" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:id="@+id/server_url" - android:inputType="textWebEmailAddress" /> - - </com.google.android.material.textfield.TextInputLayout> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/label_server_port" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_margin="8dp" - android:hint="@string/server_port_hint" - app:layout_constraintStart_toEndOf="@id/label_server_url" - app:layout_constraintTop_toBottomOf="@id/server_user_layout" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintDimensionRatio="1:0.8"> - - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/server_port" + android:layout_margin="8dp" + app:layout_constraintTop_toBottomOf="@id/label_server_protocol" + app:layout_constraintStart_toStartOf="parent" + app:selectionRequired="true" + app:singleSelection="true"> + + <com.google.android.material.button.MaterialButton + style="?attr/materialButtonOutlinedStyle" + android:id="@+id/clone_protocol_ssh" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/clone_protocol_ssh" + android:textColor="?android:attr/textColorPrimary" + app:rippleColor="@color/ripple_color" + app:strokeColor="?attr/colorSecondary" + app:backgroundTint="@color/toggle_button_selector" /> + + <com.google.android.material.button.MaterialButton + style="?attr/materialButtonOutlinedStyle" + android:id="@+id/clone_protocol_https" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/clone_protocol_https" + android:textColor="?android:attr/textColorPrimary" + app:rippleColor="@color/ripple_color" + app:strokeColor="?attr/colorSecondary" + app:backgroundTint="@color/toggle_button_selector" /> + </com.google.android.material.button.MaterialButtonToggleGroup> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/server_user_layout" + android:hint="@string/server_user" android:layout_width="match_parent" android:layout_height="wrap_content" - android:inputType="number" /> - - </com.google.android.material.textfield.TextInputLayout> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/label_server_path" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_margin="8dp" - android:hint="@string/server_path" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/label_server_url"> - - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/server_path" + android:layout_margin="8dp" + app:layout_constraintTop_toBottomOf="@id/clone_protocol_group"> + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/server_user" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textWebEmailAddress" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/label_server_url" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:hint="@string/server_url" + app:layout_constraintTop_toBottomOf="@id/server_user_layout" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/label_server_port"> + + <com.google.android.material.textfield.TextInputEditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@+id/server_url" + android:inputType="textWebEmailAddress" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/label_server_port" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:hint="@string/server_port_hint" + app:layout_constraintStart_toEndOf="@id/label_server_url" + app:layout_constraintTop_toBottomOf="@id/server_user_layout" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintDimensionRatio="1:0.8"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/server_port" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="number" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/label_server_path" android:layout_width="match_parent" android:layout_height="wrap_content" - android:inputType="textWebEmailAddress"/> - - </com.google.android.material.textfield.TextInputLayout> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/label_clone_uri" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/repository_uri" - android:editable="false" - android:layout_margin="8dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/label_server_path"> - - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/clone_uri" + android:layout_margin="8dp" + android:hint="@string/server_path" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/label_server_url"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/server_path" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textWebEmailAddress"/> + + </com.google.android.material.textfield.TextInputLayout> + + <androidx.appcompat.widget.AppCompatTextView + style="@style/TextAppearance.MaterialComponents.Headline6" + android:id="@+id/label_connection_mode" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/connection_mode" + android:layout_margin="8dp" + android:layout_marginTop="16dp" + android:layout_marginBottom="16dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/label_server_path" /> + + <com.google.android.material.button.MaterialButtonToggleGroup + android:id="@+id/connection_mode_group" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="8dp" + app:layout_constraintTop_toBottomOf="@id/label_connection_mode" + app:layout_constraintStart_toStartOf="parent" + app:selectionRequired="true" + app:singleSelection="true" > + + <com.google.android.material.button.MaterialButton + style="?attr/materialButtonOutlinedStyle" + android:id="@+id/connection_mode_ssh_key" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/connection_mode_ssh_key" + android:textColor="?android:attr/textColorPrimary" + app:rippleColor="@color/ripple_color" + app:strokeColor="?attr/colorSecondary" + app:backgroundTint="@color/toggle_button_selector" /> + + <com.google.android.material.button.MaterialButton + style="?attr/materialButtonOutlinedStyle" + android:id="@+id/connection_mode_password" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/connection_mode_basic_authentication" + android:textColor="?android:attr/textColorPrimary" + app:rippleColor="@color/ripple_color" + app:strokeColor="?attr/colorSecondary" + app:backgroundTint="@color/toggle_button_selector" /> + + <com.google.android.material.button.MaterialButton + style="?attr/materialButtonOutlinedStyle" + android:id="@+id/connection_mode_open_keychain" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/connection_mode_openkeychain" + android:textColor="?android:attr/textColorPrimary" + app:rippleColor="@color/ripple_color" + app:strokeColor="?attr/colorSecondary" + app:backgroundTint="@color/toggle_button_selector" /> + + </com.google.android.material.button.MaterialButtonToggleGroup> + + <com.google.android.material.button.MaterialButton + style="@style/Widget.MaterialComponents.Button" + android:id="@+id/save_button" + android:text="@string/crypto_save" android:layout_width="match_parent" android:layout_height="wrap_content" - android:inputType="textWebEmailAddress"/> - - </com.google.android.material.textfield.TextInputLayout> - - <androidx.appcompat.widget.AppCompatTextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@drawable/red_rectangle" - android:textColor="@android:color/white" - android:visibility="gone" - android:id="@+id/warn_url" - android:layout_margin="8dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/label_clone_uri"/> - - <androidx.appcompat.widget.AppCompatTextView - android:id="@+id/label_connection_mode" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/connection_mode" - android:layout_margin="16dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/warn_url" /> - - <Spinner - android:id="@+id/connection_mode" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="16dp" - app:layout_constraintTop_toBottomOf="@id/warn_url" - app:layout_constraintStart_toEndOf="@id/label_connection_mode" /> - - <com.google.android.material.button.MaterialButton - style="@style/Widget.MaterialComponents.Button" - android:id="@+id/clone_button" - android:text="@string/clone_button" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:onClick="cloneRepository" - android:textColor="?android:attr/windowBackground" - android:layout_marginTop="8dp" - app:backgroundTint="?attr/colorSecondary" - app:layout_constraintTop_toBottomOf="@id/label_connection_mode" - app:layout_constraintStart_toStartOf="parent" /> - - <com.google.android.material.button.MaterialButton - style="@style/Widget.MaterialComponents.Button" - android:id="@+id/save_button" - android:text="@string/crypto_save" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:onClick="saveConfiguration" - android:textColor="?android:attr/windowBackground" - android:layout_marginTop="8dp" - app:backgroundTint="?attr/colorSecondary" - app:layout_constraintTop_toBottomOf="@id/label_connection_mode" - app:layout_constraintStart_toStartOf="parent" /> -</androidx.constraintlayout.widget.ConstraintLayout> + android:textColor="?android:attr/windowBackground" + android:layout_marginTop="8dp" + app:backgroundTint="?attr/colorSecondary" + app:layout_constraintTop_toBottomOf="@id/connection_mode_group" + app:layout_constraintStart_toStartOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> +</ScrollView> diff --git a/app/src/main/res/layout/activity_git_config.xml b/app/src/main/res/layout/activity_git_config.xml index a576ec46..94c09e27 100644 --- a/app/src/main/res/layout/activity_git_config.xml +++ b/app/src/main/res/layout/activity_git_config.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent" android:padding="@dimen/activity_horizontal_margin" android:background="?android:attr/windowBackground" - tools:context="com.zeapo.pwdstore.git.GitActivity" + tools:context="com.zeapo.pwdstore.git.GitConfigActivity" tools:layout_editor_absoluteX="0dp" tools:layout_editor_absoluteY="81dp"> @@ -51,7 +51,6 @@ android:layout_height="wrap_content" android:layout_margin="8dp" android:text="@string/crypto_save" - android:onClick="applyGitConfigs" android:textColor="?android:attr/windowBackground" app:backgroundTint="?attr/colorSecondary" app:layout_constraintTop_toBottomOf="@id/email_input_layout" @@ -95,7 +94,6 @@ android:layout_height="wrap_content" android:layout_margin="8dp" android:text="@string/abort_rebase" - android:onClick="abortRebase" android:textColor="?android:attr/windowBackground" app:backgroundTint="?attr/colorSecondary" app:layout_constraintTop_toBottomOf="@id/commit_hash_label" /> @@ -107,7 +105,6 @@ android:layout_height="wrap_content" android:layout_margin="8dp" android:text="@string/reset_to_remote" - android:onClick="resetToRemote" android:textColor="?android:attr/windowBackground" app:backgroundTint="?attr/colorSecondary" app:layout_constraintTop_toBottomOf="@id/git_abort_rebase" /> diff --git a/app/src/main/res/layout/autofill_instructions.xml b/app/src/main/res/layout/autofill_instructions.xml index 8ce0a793..e7cf3dd1 100644 --- a/app/src/main/res/layout/autofill_instructions.xml +++ b/app/src/main/res/layout/autofill_instructions.xml @@ -19,7 +19,6 @@ android:textSize="16sp" /> <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/imageView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="8dp" diff --git a/app/src/main/res/layout/git_passphrase_layout.xml b/app/src/main/res/layout/git_passphrase_layout.xml index 814f4e40..c0922471 100644 --- a/app/src/main/res/layout/git_passphrase_layout.xml +++ b/app/src/main/res/layout/git_passphrase_layout.xml @@ -2,31 +2,31 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:padding="16dp"> - <EditText - android:id="@+id/sshkey_passphrase" - android:layout_width="0dp" + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/git_auth_passphrase_layout" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginEnd="16dp" - android:layout_marginStart="16dp" - android:layout_marginTop="8dp" - android:ems="10" - android:inputType="textPassword" - android:importantForAccessibility="no" + app:hintEnabled="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent"> + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/git_auth_passphrase" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/ssh_keygen_passphrase" + android:inputType="textPassword" /> + </com.google.android.material.textfield.TextInputLayout> - <CheckBox - android:id="@+id/sshkey_remember_passphrase" - android:layout_width="0dp" + <com.google.android.material.checkbox.MaterialCheckBox + android:id="@+id/git_auth_remember_passphrase" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="16dp" - android:layout_marginStart="16dp" - android:layout_marginTop="8dp" android:text="@string/remember_the_passphrase" app:layout_constraintRight_toRightOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/sshkey_passphrase" /> + app:layout_constraintTop_toBottomOf="@+id/git_auth_passphrase_layout" /> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/password_recycler_view.xml b/app/src/main/res/layout/password_recycler_view.xml index dd6b00c1..5368d593 100644 --- a/app/src/main/res/layout/password_recycler_view.xml +++ b/app/src/main/res/layout/password_recycler_view.xml @@ -25,7 +25,6 @@ </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> <LinearLayout - android:id="@+id/create_options" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="top|center_horizontal" diff --git a/app/src/main/res/menu/git_clone.xml b/app/src/main/res/menu/git_clone.xml index 092a8b2b..d170cca8 100644 --- a/app/src/main/res/menu/git_clone.xml +++ b/app/src/main/res/menu/git_clone.xml @@ -1,7 +1,7 @@ <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:pwstore="http://schemas.android.com/apk/res-auto" - tools:context="com.zeapo.pwdstore.git.GitActivity" > + tools:context="com.zeapo.pwdstore.git.GitServerConfigActivity" > <item android:id="@+id/user_pref" android:title="@string/action_settings" android:orderInCategory="100" diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 7720784b..15ec0a8e 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -112,7 +112,6 @@ <string name="dialog_ok">حسناً</string> <string name="dialog_yes">نعم</string> <string name="dialog_no">لا</string> - <string name="dialog_negative">لا … لاحقاً</string> <string name="dialog_cancel">إلغاء</string> <string name="git_sync">زامن المستودع</string> <string name="show_password_pref_title">إظهار كلمة السر</string> diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 86985f8f..5917ff6b 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -9,7 +9,6 @@ <string name="title_activity_git_clone">Informace repozitáře</string> <!-- Password Store --> <string name="creation_dialog_text">Naklonujte nebo vytvořte nový repozitář před pokusem přidat heslo nebo spustit synchronizaci.</string> - <string name="key_dialog_text">Před inicializací repozitáře je třeba vybrat "ID PGP klíče"</string> <string name="delete_dialog_text">Opravdu chcete smazat heslo %1$s?</string> <string name="move">Přesunout</string> <string name="edit">Editovat</string> @@ -43,7 +42,6 @@ <!-- Git Handler --> <string name="forget_username_dialog_text">Zapomněli jste uvést přihlašovací jméno?</string> - <string name="set_information_dialog_text">Je třeba zadat informaci o serveru před vlastní synchronizací</string> <string name="ssh_preferences_dialog_text">Importujte nebo si prosím vygenerujte svůj SSH klíč v nastavení aplikace</string> <string name="ssh_preferences_dialog_title">Žádný SSH klíč</string> <string name="ssh_preferences_dialog_import">Import</string> @@ -163,8 +161,6 @@ <string name="dialog_ok">OK</string> <string name="dialog_yes">Ano</string> <string name="dialog_no">Ne</string> - <string name="dialog_positive">Je na cestě…</string> - <string name="dialog_negative">Ne… později</string> <string name="dialog_oops">Ajaj…</string> <string name="dialog_cancel">Zrušit</string> <string name="git_sync">Synchronizovat repozitář</string> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4960387f..aec047f9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -9,7 +9,6 @@ <string name="title_activity_git_clone">Repository Informationen</string> <!-- Password Store --> <string name="creation_dialog_text">Bitte klone oder erstelle ein neues Repository, bevor du versuchst ein Passwort hinzuzufügen oder jegliche Synchronisation-Operation durchführst.</string> - <string name="key_dialog_text">Du musst deine PGP-Key ID auwählen, bevor das Repository intialisiert wird.</string> <string name="delete_dialog_text">Bist du dir sicher, dass du das Passwort löschen möchtest %1$s?</string> <string name="move">Verschieben</string> <string name="edit">Bearbeiten</string> @@ -28,7 +27,6 @@ <!-- Git Handler --> <string name="forget_username_dialog_text">Hast du vergessen einen Nutzernamen zu vergeben?</string> - <string name="set_information_dialog_text">You have to set the information about the server before synchronizing with the server</string> <string name="ssh_preferences_dialog_text">Please import or generate your SSH key file in the preferences</string> <string name="ssh_preferences_dialog_title">Kein SSH-Key angegeben</string> <string name="ssh_preferences_dialog_import">Import</string> @@ -141,8 +139,6 @@ <string name="dialog_ok">OK</string> <string name="dialog_yes">Ja</string> <string name="dialog_no">Nein</string> - <string name="dialog_positive">Auf dem Weg…</string> - <string name="dialog_negative">Nah… später</string> <string name="dialog_oops">Oops…</string> <string name="dialog_cancel">Abbruch</string> <string name="git_sync">Synchronisiere Repository</string> diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 47ec808d..aeb0a133 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -9,7 +9,6 @@ <string name="title_activity_git_clone">Información de repositorio</string> <!-- Password Store --> <string name="creation_dialog_text">Por favor clona o crea un nuevo repositorio antes de añadir una contraseña o ejecutar una operación de sincronización.</string> - <string name="key_dialog_text">Tienes que seleccionar una llave PGP antes de inicializar el repositorio</string> <string name="delete_dialog_text">Confirma que deseas eliminar la contraseña %1$s</string> <string name="move">Mover</string> <string name="edit">Editar</string> @@ -39,7 +38,6 @@ <!-- Git Handler --> <string name="forget_username_dialog_text">Olvidaste especificar un nombre de usuario?</string> - <string name="set_information_dialog_text">Necesitas configurar la información del servidor antes de sincronizar</string> <string name="ssh_preferences_dialog_text">Por favor importa o genera tu llave SSH en los ajustes</string> <string name="ssh_preferences_dialog_title">No hay llave SSH</string> <string name="ssh_preferences_dialog_import">Importar</string> @@ -177,8 +175,6 @@ <string name="dialog_ok">OK</string> <string name="dialog_yes">Sí</string> <string name="dialog_no">No</string> - <string name="dialog_positive">Ok, Vamos…</string> - <string name="dialog_negative">Nah… después</string> <string name="dialog_oops">Ups…</string> <string name="dialog_cancel">Cancelar</string> <string name="git_sync">Sincronizar con servidor</string> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0f4c2025..8461d96d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -9,7 +9,6 @@ <string name="title_activity_git_clone">Information sur le dépôt Git</string> <!-- Password Store --> <string name="creation_dialog_text">Clonez ou créez un dépôt suivant avant d\'essayer d\'ajouter un mot de pass ou d\'effectuer une opération de synchornisation.</string> - <string name="key_dialog_text">Vous devez sélectionner votre "PGP-Key ID" avant d\'initialiser le dépôt</string> <string name="delete_dialog_text">Êtes-vous sûr de vouloir supprimer le mot de passe %1$s?</string> <string name="move">Déplacer</string> <string name="edit">Éditer</string> @@ -45,7 +44,6 @@ <!-- Git Handler --> <string name="forget_username_dialog_text">Avez-vous oublié to renseigner votre nom d\'utilisateur ?</string> - <string name="set_information_dialog_text">Vous devez renseignez les informations à propos du serveur avant d\'effectuer une synchronisation avec celui-ci</string> <string name="ssh_preferences_dialog_text">Vous devez importer ou générer votre fichier de clef SSH dans les préférences</string> <string name="ssh_preferences_dialog_title">Absence de cled SSH</string> <string name="ssh_preferences_dialog_import">Importer</string> @@ -178,8 +176,6 @@ <string name="dialog_ok">OK</string> <string name="dialog_yes">Oui</string> <string name="dialog_no">Non</string> - <string name="dialog_positive">En chemin…</string> - <string name="dialog_negative">Non… plus tard</string> <string name="dialog_oops">Oups…</string> <string name="dialog_cancel">Annuler</string> <string name="git_sync">Synchronisation du dépôt</string> diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c798f6c2..a70c5670 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -9,7 +9,6 @@ <string name="title_activity_git_clone">リポジトリ情報</string> <!-- Password Store --> <string name="creation_dialog_text">パスワードや同期操作を追加する前に、以下の新しいリポジトリをクローンまたは作成してください。</string> - <string name="key_dialog_text">リポジトリを初期化する前に "PGP 鍵 ID"を選択する必要があります</string> <string name="delete_dialog_text">パスワードを削除してもよろしいですか %1$s</string> <string name="delete">削除</string> <!-- git commits --> @@ -28,7 +27,6 @@ <!-- Git Handler --> <string name="forget_username_dialog_text">ユーザー名の指定を忘れましたか?</string> - <string name="set_information_dialog_text">サーバーと同期する前に、サーバーに関する情報を設定する必要があります</string> <string name="ssh_preferences_dialog_text">プリファレンスで SSH 鍵ファイルをインポートまたは生成してください</string> <string name="ssh_preferences_dialog_title">SSH 鍵がありませんkey</string> <string name="ssh_preferences_dialog_import">インポート</string> @@ -120,8 +118,6 @@ <string name="dialog_ok">OK</string> <string name="dialog_yes">はい</string> <string name="dialog_no">いいえ</string> - <string name="dialog_positive">途中…</string> - <string name="dialog_negative">いや…あとで</string> <string name="dialog_oops">おっと…</string> <string name="dialog_cancel">キャンセル</string> <string name="git_sync">リポジトリを同期</string> diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 9dfc9d73..0b858c75 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -16,4 +16,6 @@ <color name="navigation_bar_color">@color/primary_color</color> <color name="list_multiselect_background">#66EEEEEE</color> <color name="status_bar_color">@color/window_background</color> + <color name="ripple_color">#aaff7539</color> + <color name="button_color">#44ff7539</color> </resources> diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bc73c5c2..04d7b837 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -9,7 +9,6 @@ <string name="title_activity_git_clone">Информация о репозитории</string> <!-- Password Store --> <string name="creation_dialog_text">Пожалуйста, клонируйте или создайте новый репозиторий перед тем, как добавлять пароль или выполнять синхронизацию.</string> - <string name="key_dialog_text">Вы должны выбрать PGP ключ перед инициализацией хранилища</string> <string name="delete_dialog_text">Вы уверены что хотите удалить пароль %1$s</string> <string name="move">Переместить</string> <string name="edit">Редактировать</string> @@ -47,7 +46,6 @@ <!-- Git Handler --> <string name="forget_username_dialog_text">Вы забыли указать имя пользователя?</string> - <string name="set_information_dialog_text">Вы должны указать информацию о сервере до выполнения синхронизации</string> <string name="ssh_preferences_dialog_text">Пожалуйста, импортируйте или сгенерируйте новый SSH ключ в настройках</string> <string name="ssh_preferences_dialog_title">Нет SSH ключа</string> <string name="ssh_preferences_dialog_import">Импортировать</string> @@ -211,8 +209,6 @@ <string name="dialog_ok">OK</string> <string name="dialog_yes">Да</string> <string name="dialog_no">Нет</string> - <string name="dialog_positive">On my way…</string> - <string name="dialog_negative">Не … позже</string> <string name="dialog_oops">Упс…</string> <string name="dialog_cancel">Отмена</string> <string name="git_sync">Синхронизировать репозиторий</string> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 019f703c..76926ce9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -9,7 +9,6 @@ <string name="title_activity_git_clone">Repo 信息</string> <!-- Password Store --> <string name="creation_dialog_text">在尝试添加密码或任何同步操作前请在下方克隆或添加一个新的 Repo</string> - <string name="key_dialog_text">在初始化 Repo 之前你必须选择你的\"PGP-Key ID\"</string> <string name="delete_dialog_text">你确定要删除密码 %1$s</string> <string name="delete">删除</string> <!-- git commits --> @@ -28,7 +27,6 @@ <!-- Git Handler --> <string name="forget_username_dialog_text">你忘了提供用户名了吗?</string> - <string name="set_information_dialog_text">你必须在与服务器同步前设置服务器信息</string> <string name="ssh_preferences_dialog_text">请在设置中导入或生成你的SSH密钥文件</string> <string name="ssh_preferences_dialog_title">无SSH密钥</string> <string name="ssh_preferences_dialog_import">导入</string> @@ -117,8 +115,6 @@ <string name="dialog_ok">确定</string> <string name="dialog_yes">确定</string> <string name="dialog_no">否</string> - <string name="dialog_positive">现在就去</string> - <string name="dialog_negative">呃… 算了吧</string> <string name="dialog_oops">糟糕…</string> <string name="dialog_cancel">取消</string> <string name="git_sync">同步 Repo</string> diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index bdc6f1fb..7a4b4f1f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -9,7 +9,6 @@ <string name="title_activity_git_clone">Repo 訊息</string> <!-- Password Store --> <string name="creation_dialog_text">在嘗試新增密碼或任何同步操作之前請在下方 clone 或新增一個新的 Repo</string> - <string name="key_dialog_text">在初始化 Repo 之前你必須選擇你的\"PGP-Key ID\"</string> <string name="delete_dialog_text">你確定要刪除密碼 %1$s</string> <string name="delete">刪除</string> <!-- PGPHandler --> @@ -25,7 +24,6 @@ <!-- Git Handler --> <string name="forget_username_dialog_text">你忘記輸入使用者名稱了嗎?</string> - <string name="set_information_dialog_text">你必須在與伺服器同步前設定伺服器資訊</string> <string name="ssh_preferences_dialog_text">請在設定中匯入或產生你的 SSH 金鑰</string> <string name="ssh_preferences_dialog_title">無 SSH 金鑰</string> <string name="ssh_preferences_dialog_import">匯入</string> @@ -114,8 +112,6 @@ <string name="dialog_ok">確定</string> <string name="dialog_yes">確定</string> <string name="dialog_no">否</string> - <string name="dialog_positive">確定</string> - <string name="dialog_negative">呃… 算了吧</string> <string name="dialog_oops">糟糕…</string> <string name="dialog_cancel">取消</string> <string name="git_sync">同步 Repo</string> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 13bc049d..54a3af70 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -19,6 +19,8 @@ <color name="list_multiselect_background">#668eacbb</color> <color name="navigation_bar_color">#000000</color> <color name="status_bar_color">@color/primary_dark_color</color> + <color name="ripple_color">#aaff7043</color> + <color name="button_color">#44ff7043</color> <!-- Override TextInputEditText stroke color like a boss --> <color name="mtrl_textinput_default_box_stroke_color" tools:override="true"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bd2aa75e..dafad85a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,7 +19,7 @@ <!-- Password Store --> <string name="creation_dialog_text">Please clone or create a new repository below before trying to add a password or running any synchronization operation.</string> - <string name="key_dialog_text">You have to select your PGP key ID before initializing the repository</string> + <string name="key_dialog_text">A valid PGP key must be selected in Settings before initializing the repository</string> <string name="delete_dialog_text">Are you sure you want to delete the password %1$s?</string> <string name="move">Move</string> <string name="edit">Edit</string> @@ -57,7 +57,7 @@ <!-- Git Handler --> <string name="forget_username_dialog_text">Did you forget to specify a username?</string> - <string name="set_information_dialog_text">You have to set the information about the server before synchronizing with the server</string> + <string name="set_information_dialog_text">Please fix the remote server configuration in settings before proceeding</string> <string name="ssh_preferences_dialog_text">Please import or generate your SSH key file in the preferences</string> <string name="ssh_preferences_dialog_title">No SSH key</string> <string name="ssh_preferences_dialog_import">Import</string> @@ -228,8 +228,8 @@ <string name="dialog_ok">OK</string> <string name="dialog_yes">Yes</string> <string name="dialog_no">No</string> - <string name="dialog_positive">On my way…</string> - <string name="dialog_negative">Nah… later</string> + <string name="dialog_positive">Go to Settings</string> + <string name="dialog_negative">Go back</string> <string name="dialog_oops">Oops…</string> <string name="dialog_cancel">Cancel</string> <string name="git_sync">Synchronize repository</string> @@ -309,7 +309,7 @@ <string name="jgit_error_push_dialog_text">Error occurred during the push operation:</string> <string name="ssh_key_clear_passphrase">Clear ssh-key saved passphrase</string> <string name="hotp_remember_clear_choice">Clear saved preference for HOTP incrementing</string> - <string name="remember_the_passphrase">Remember the passphrase in the app configuration (insecure)</string> + <string name="remember_the_passphrase">Remember key passphrase</string> <string name="hackish_tools">Hackish tools</string> <string name="abort_rebase">Abort rebase and push new branch</string> <string name="reset_to_remote">Hard reset to remote branch</string> @@ -349,4 +349,14 @@ <string name="theme_dark">Dark</string> <string name="theme_battery_saver">Set by Battery Saver</string> <string name="theme_follow_system">System default</string> + <string name="clone_protocol_ssh" translatable="false">SSH</string> + <string name="clone_protocol_https" translatable="false">HTTPS</string> + <string name="connection_mode_ssh_key" translatable="false">SSH key</string> + <string name="connection_mode_basic_authentication" translatable="false">Password</string> + <string name="connection_mode_openkeychain" translatable="false">OpenKeychain</string> + <string name="git_server_config_save_success">Successfully saved configuration</string> + <string name="git_server_config_save_failure">Configuration error: please verify your settings and try again</string> + <string name="git_operation_unable_to_open_ssh_key_title">Unable to open the ssh-key</string> + <string name="git_operation_unable_to_open_ssh_key_message">Please check that it was imported.</string> + <string name="git_operation_wrong_passphrase">Wrong passphrase</string> </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 090b56b9..b5a03bb4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -33,6 +33,21 @@ <item name="background">@color/primary_color</item> </style> + <style name="NoBackgroundTheme" parent="@style/AppTheme"> + <item name="android:background">@android:color/transparent</item> + <item name="android:backgroundDimEnabled">true</item> + <item name="android:navigationBarColor">@android:color/transparent</item> + <item name="android:statusBarColor">@android:color/transparent</item> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:windowActionBar">false</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowEnterAnimation">@android:anim/fade_in</item> + <item name="android:windowExitAnimation">@android:anim/fade_out</item> + <item name="colorPrimaryDark">@android:color/transparent</item> + <item name="windowNoTitle">true</item> + </style> + <style name="ThemeOverlay.AppTheme.TextInputEditText.OutlinedBox" parent="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox"> <item name="colorControlActivated">@color/color_control_normal</item> </style> diff --git a/dependencies.gradle b/dependencies.gradle index d50129a6..fb97b30c 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -43,6 +43,7 @@ ext.deps = [ preference: 'androidx.preference:preference:1.1.0', recycler_view: 'androidx.recyclerview:recyclerview:1.2.0-alpha02', recycler_view_selection: 'androidx.recyclerview:recyclerview-selection:1.1.0-rc01', + security: 'androidx.security:security-crypto:1.0.0-beta01', swiperefreshlayout: 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-beta01' ], |