diff options
Diffstat (limited to 'app/src/main/java')
30 files changed, 56 insertions, 1902 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt deleted file mode 100644 index 1017d3a9..00000000 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package app.passwordstore.ui.autofill - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.IntentSender -import android.os.Build -import android.os.Bundle -import android.view.autofill.AutofillManager -import android.widget.Toast -import androidx.activity.result.IntentSenderRequest -import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult -import androidx.annotation.RequiresApi -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope -import app.passwordstore.data.passfile.PasswordEntry -import app.passwordstore.util.autofill.AutofillPreferences -import app.passwordstore.util.autofill.AutofillResponseBuilder -import app.passwordstore.util.autofill.DirectoryStructure -import app.passwordstore.util.extensions.OPENPGP_PROVIDER -import app.passwordstore.util.extensions.asLog -import com.github.androidpasswordstore.autofillparser.AutofillAction -import com.github.androidpasswordstore.autofillparser.Credentials -import com.github.michaelbull.result.getOrElse -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.onSuccess -import com.github.michaelbull.result.runCatching -import dagger.hilt.android.AndroidEntryPoint -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.InputStream -import java.io.OutputStream -import javax.inject.Inject -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import logcat.LogPriority.ERROR -import logcat.logcat -import me.msfjarvis.openpgpktx.util.OpenPgpApi -import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection -import org.openintents.openpgp.IOpenPgpService2 -import org.openintents.openpgp.OpenPgpError - -@RequiresApi(26) -@AndroidEntryPoint -class AutofillDecryptActivity : AppCompatActivity() { - - companion object { - - private const val EXTRA_FILE_PATH = "app.passwordstore.autofill.oreo.EXTRA_FILE_PATH" - private const val EXTRA_SEARCH_ACTION = "app.passwordstore.autofill.oreo.EXTRA_SEARCH_ACTION" - - private var decryptFileRequestCode = 1 - - fun makeDecryptFileIntent(file: File, forwardedExtras: Bundle, context: Context): Intent { - return Intent(context, AutofillDecryptActivity::class.java).apply { - putExtras(forwardedExtras) - putExtra(EXTRA_SEARCH_ACTION, true) - putExtra(EXTRA_FILE_PATH, file.absolutePath) - } - } - - fun makeDecryptFileIntentSender(file: File, context: Context): IntentSender { - val intent = - Intent(context, AutofillDecryptActivity::class.java).apply { - putExtra(EXTRA_SEARCH_ACTION, false) - putExtra(EXTRA_FILE_PATH, file.absolutePath) - } - return PendingIntent.getActivity( - context, - decryptFileRequestCode++, - intent, - if (Build.VERSION.SDK_INT >= 31) { - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE - } else { - PendingIntent.FLAG_CANCEL_CURRENT - }, - ) - .intentSender - } - } - - @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - - private val decryptInteractionRequiredAction = - registerForActivityResult(StartIntentSenderForResult()) { result -> - if (continueAfterUserInteraction != null) { - val data = result.data - if (result.resultCode == RESULT_OK && data != null) { - continueAfterUserInteraction?.resume(data) - } else { - continueAfterUserInteraction?.resumeWithException( - Exception("OpenPgpApi ACTION_DECRYPT_VERIFY failed to continue after user interaction") - ) - } - continueAfterUserInteraction = null - } - } - - private var continueAfterUserInteraction: Continuation<Intent>? = null - private lateinit var directoryStructure: DirectoryStructure - - override fun onStart() { - super.onStart() - val filePath = - intent?.getStringExtra(EXTRA_FILE_PATH) - ?: run { - logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_FILE_PATH" } - finish() - return - } - val clientState = - intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) - ?: run { - logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" } - finish() - return - } - val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!! - val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match - directoryStructure = AutofillPreferences.directoryStructure(this) - logcat { action.toString() } - lifecycleScope.launch { - val credentials = decryptCredential(File(filePath)) - if (credentials == null) { - setResult(RESULT_CANCELED) - } else { - val fillInDataset = - AutofillResponseBuilder.makeFillInDataset( - this@AutofillDecryptActivity, - credentials, - clientState, - action - ) - withContext(Dispatchers.Main) { - setResult( - RESULT_OK, - Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) } - ) - } - } - withContext(Dispatchers.Main) { finish() } - } - } - - private suspend fun executeOpenPgpApi( - data: Intent, - input: InputStream, - output: OutputStream - ): Intent { - var openPgpServiceConnection: OpenPgpServiceConnection? = null - val openPgpService = - suspendCoroutine<IOpenPgpService2> { cont -> - openPgpServiceConnection = - OpenPgpServiceConnection( - this, - OPENPGP_PROVIDER, - object : OpenPgpServiceConnection.OnBound { - override fun onBound(service: IOpenPgpService2) { - cont.resume(service) - } - - override fun onError(e: Exception) { - cont.resumeWithException(e) - } - } - ) - .also { it.bindToService() } - } - return OpenPgpApi(this, openPgpService).executeApi(data, input, output).also { - openPgpServiceConnection?.unbindFromService() - } - } - - private suspend fun decryptCredential(file: File, resumeIntent: Intent? = null): Credentials? { - val command = resumeIntent ?: Intent().apply { action = OpenPgpApi.ACTION_DECRYPT_VERIFY } - runCatching { file.inputStream() } - .onFailure { e -> - logcat(ERROR) { e.asLog("File to decrypt not found") } - return null - } - .onSuccess { encryptedInput -> - val decryptedOutput = ByteArrayOutputStream() - runCatching { executeOpenPgpApi(command, encryptedInput, decryptedOutput) } - .onFailure { e -> - logcat(ERROR) { e.asLog("OpenPgpApi ACTION_DECRYPT_VERIFY failed") } - return null - } - .onSuccess { result -> - return when ( - val resultCode = - result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR) - ) { - OpenPgpApi.RESULT_CODE_SUCCESS -> { - runCatching { - val entry = - withContext(Dispatchers.IO) { - @Suppress("BlockingMethodInNonBlockingContext") - passwordEntryFactory.create(decryptedOutput.toByteArray()) - } - AutofillPreferences.credentialsFromStoreEntry( - this, - file, - entry, - directoryStructure - ) - } - .getOrElse { e -> - logcat(ERROR) { e.asLog("Failed to parse password entry") } - return null - } - } - OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - val pendingIntent: PendingIntent = - result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!! - runCatching { - val intentToResume = - withContext(Dispatchers.Main) { - suspendCoroutine<Intent> { cont -> - continueAfterUserInteraction = cont - decryptInteractionRequiredAction.launch( - IntentSenderRequest.Builder(pendingIntent.intentSender).build() - ) - } - } - decryptCredential(file, intentToResume) - } - .getOrElse { e -> - logcat(ERROR) { - e.asLog("OpenPgpApi ACTION_DECRYPT_VERIFY failed with user interaction") - } - return null - } - } - OpenPgpApi.RESULT_CODE_ERROR -> { - val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR) - if (error != null) { - withContext(Dispatchers.Main) { - Toast.makeText( - applicationContext, - "Error from OpenKeyChain: ${error.message}", - Toast.LENGTH_LONG - ) - .show() - } - logcat(ERROR) { - "OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}" - } - } - null - } - else -> { - logcat(ERROR) { "Unrecognized OpenPgpApi result: $resultCode" } - null - } - } - } - } - return null - } -} diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt index a5a0d34e..65ec4229 100644 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillSaveActivity.kt @@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.os.bundleOf import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.ui.crypto.PasswordCreationActivity import app.passwordstore.ui.crypto.PasswordCreationActivityV2 import app.passwordstore.util.autofill.AutofillMatcher import app.passwordstore.util.autofill.AutofillPreferences @@ -114,9 +113,9 @@ class AutofillSaveActivity : AppCompatActivity() { bundleOf( "REPO_PATH" to repo.absolutePath, "FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath, - PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME), - PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD), - PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to + PasswordCreationActivityV2.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME), + PasswordCreationActivityV2.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD), + PasswordCreationActivityV2.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) ) ) diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt index 7298e038..b6bb481f 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt @@ -5,12 +5,9 @@ package app.passwordstore.ui.crypto -import android.app.PendingIntent import android.content.ClipData import android.content.Intent -import android.content.IntentSender import android.content.SharedPreferences -import android.net.Uri import android.os.Build import android.os.Bundle import android.view.WindowManager @@ -19,33 +16,20 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import app.passwordstore.R import app.passwordstore.injection.prefs.SettingsPreferences -import app.passwordstore.util.extensions.OPENPGP_PROVIDER -import app.passwordstore.util.extensions.asLog import app.passwordstore.util.extensions.clipboard import app.passwordstore.util.extensions.getString import app.passwordstore.util.extensions.snackbar import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.features.Features import app.passwordstore.util.services.ClipboardService import app.passwordstore.util.settings.PreferenceKeys -import com.github.michaelbull.result.getOr -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import java.io.File import javax.inject.Inject -import logcat.LogPriority.ERROR -import logcat.LogPriority.INFO -import logcat.logcat -import me.msfjarvis.openpgpktx.util.OpenPgpApi -import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection -import org.openintents.openpgp.IOpenPgpService2 -import org.openintents.openpgp.OpenPgpError @Suppress("Registered") @AndroidEntryPoint -open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { +open class BasePgpActivity : AppCompatActivity() { /** Full path to the repository */ val repoPath by unsafeLazy { intent.getStringExtra("REPO_PATH")!! } @@ -63,20 +47,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou /** [SharedPreferences] instance used by subclasses to persist settings */ @SettingsPreferences @Inject lateinit var settings: SharedPreferences - @Inject lateinit var features: Features - - /** - * Handle to the [OpenPgpApi] instance that is used by subclasses to interface with OpenKeychain. - */ - private var serviceConnection: OpenPgpServiceConnection? = null - var api: OpenPgpApi? = null - - /** - * A [OpenPgpServiceConnection.OnBound] instance for the last listener that we wish to bind with - * in case the previous attempt was cancelled due to missing [OPENPGP_PROVIDER] package. - */ - private var previousListener: OpenPgpServiceConnection.OnBound? = null - /** * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots or * recent apps screen. @@ -88,124 +58,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou } /** - * [onDestroy] handles unbinding from the OpenPgp service linked with [serviceConnection]. This is - * annotated with [CallSuper] because it's critical to unbind the service to ensure we're not - * leaking things. - */ - @CallSuper - override fun onDestroy() { - super.onDestroy() - serviceConnection?.unbindFromService() - previousListener = null - } - - /** - * [onResume] controls the flow for resumption of a PGP operation that was previously interrupted - * by the [OPENPGP_PROVIDER] package being missing. - */ - override fun onResume() { - super.onResume() - previousListener?.let { bindToOpenKeychain(it) } - } - - /** - * Sets up [api] once the service is bound. Downstream consumers must call super this to - * initialize [api] - */ - @CallSuper - override fun onBound(service: IOpenPgpService2) { - api = OpenPgpApi(this, service) - } - - /** - * Mandatory error handling from [OpenPgpServiceConnection.OnBound]. All subclasses must handle - * their own errors, and hence this class simply logs and rethrows. Subclasses Must NOT call - * super. - */ - override fun onError(e: Exception) { - logcat(ERROR) { e.asLog("Callers must handle their own exceptions") } - throw e - } - - /** Method for subclasses to initiate binding with [OpenPgpServiceConnection]. */ - fun bindToOpenKeychain(onBoundListener: OpenPgpServiceConnection.OnBound) { - if (true) return - val installed = - runCatching { - packageManager.getPackageInfo(OPENPGP_PROVIDER, 0) - true - } - .getOr(false) - if (!installed) { - previousListener = onBoundListener - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.openkeychain_not_installed_title)) - .setMessage(getString(R.string.openkeychain_not_installed_message)) - .setPositiveButton(getString(R.string.openkeychain_not_installed_google_play)) { _, _ -> - runCatching { - val intent = - Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(getString(R.string.play_deeplink_template, OPENPGP_PROVIDER)) - setPackage("com.android.vending") - } - startActivity(intent) - } - } - .setNeutralButton(getString(R.string.openkeychain_not_installed_fdroid)) { _, _ -> - runCatching { - val intent = - Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(getString(R.string.fdroid_deeplink_template, OPENPGP_PROVIDER)) - } - startActivity(intent) - } - } - .setOnCancelListener { finish() } - .show() - return - } else { - previousListener = null - serviceConnection = - OpenPgpServiceConnection(this, OPENPGP_PROVIDER, onBoundListener).also { - it.bindToService() - } - } - } - - /** - * Handle the case where OpenKeychain returns that it needs to interact with the user - * - * @param result The intent returned by OpenKeychain - */ - fun getUserInteractionRequestIntent(result: Intent): IntentSender { - logcat(INFO) { "RESULT_CODE_USER_INTERACTION_REQUIRED" } - return result.getParcelableExtra<PendingIntent>(OpenPgpApi.RESULT_INTENT)!!.intentSender - } - - /** - * Base handling of OpenKeychain errors based on the error contained in [result]. Subclasses can - * use this when they want to default to sane error handling. - */ - fun handleError(result: Intent) { - val error: OpenPgpError? = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR) - if (error != null) { - when (error.errorId) { - OpenPgpError.NO_OR_WRONG_PASSPHRASE -> { - snackbar(message = getString(R.string.openpgp_error_wrong_passphrase)) - } - OpenPgpError.NO_USER_IDS -> { - snackbar(message = getString(R.string.openpgp_error_no_user_ids)) - } - else -> { - snackbar(message = getString(R.string.openpgp_error_unknown, error.message)) - logcat(ERROR) { "onError getErrorId: ${error.errorId}" } - logcat(ERROR) { "onError getMessage: ${error.message}" } - } - } - } - } - - /** * Copies provided [text] to the clipboard. Shows a [Snackbar] which can be disabled by passing * [showSnackbar] as false. */ @@ -251,7 +103,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou companion object { - private const val TAG = "APS/BasePgpActivity" const val EXTRA_FILE_PATH = "FILE_PATH" const val EXTRA_REPO_PATH = "REPO_PATH" diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt deleted file mode 100644 index f3fac1f2..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.crypto - -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import androidx.activity.result.IntentSenderRequest -import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult -import androidx.lifecycle.lifecycleScope -import app.passwordstore.R -import app.passwordstore.data.passfile.PasswordEntry -import app.passwordstore.data.password.FieldItem -import app.passwordstore.databinding.DecryptLayoutBinding -import app.passwordstore.ui.adapters.FieldItemAdapter -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.settings.PreferenceKeys -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import dagger.hilt.android.AndroidEntryPoint -import java.io.ByteArrayOutputStream -import java.io.File -import javax.inject.Inject -import kotlin.time.Duration -import kotlin.time.ExperimentalTime -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat -import me.msfjarvis.openpgpktx.util.OpenPgpApi -import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection -import org.openintents.openpgp.IOpenPgpService2 - -@AndroidEntryPoint -class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound { - - private val binding by viewBinding(DecryptLayoutBinding::inflate) - @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - - private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) } - private var passwordEntry: PasswordEntry? = null - - private val userInteractionRequiredResult = - registerForActivityResult(StartIntentSenderForResult()) { result -> - if (result.data == null) { - setResult(RESULT_CANCELED, null) - finish() - return@registerForActivityResult - } - - when (result.resultCode) { - RESULT_OK -> decryptAndVerify(result.data) - RESULT_CANCELED -> { - setResult(RESULT_CANCELED, result.data) - finish() - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - bindToOpenKeychain(this) - title = name - with(binding) { - setContentView(root) - passwordCategory.text = relativeParentPath - passwordFile.text = name - passwordFile.setOnLongClickListener { - copyTextToClipboard(name) - true - } - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.pgp_handler, menu) - passwordEntry?.let { entry -> - menu.findItem(R.id.edit_password).isVisible = true - if (!entry.password.isNullOrBlank()) { - menu.findItem(R.id.share_password_as_plaintext).isVisible = true - menu.findItem(R.id.copy_password).isVisible = true - } - } - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> onBackPressed() - R.id.edit_password -> editPassword() - R.id.share_password_as_plaintext -> shareAsPlaintext() - R.id.copy_password -> copyPasswordToClipboard(passwordEntry?.password) - else -> return super.onOptionsItemSelected(item) - } - return true - } - - override fun onBound(service: IOpenPgpService2) { - super.onBound(service) - decryptAndVerify() - } - - override fun onError(e: Exception) { - logcat(ERROR) { e.asLog() } - } - - /** - * Automatically finishes the activity 60 seconds after decryption succeeded to prevent - * information leaks from stale activities. - */ - @OptIn(ExperimentalTime::class) - private fun startAutoDismissTimer() { - lifecycleScope.launch { - delay(Duration.seconds(60)) - finish() - } - } - - /** - * Edit the current password and hide all the fields populated by encrypted data so that when the - * result triggers they can be repopulated with new data. - */ - private fun editPassword() { - val intent = Intent(this, PasswordCreationActivity::class.java) - intent.putExtra("FILE_PATH", relativeParentPath) - intent.putExtra("REPO_PATH", repoPath) - intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name) - intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password) - intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContentString) - intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true) - startActivity(intent) - finish() - } - - private fun shareAsPlaintext() { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, passwordEntry?.password) - type = "text/plain" - } - // Always show a picker to give the user a chance to cancel - startActivity( - Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to)) - ) - } - - @OptIn(ExperimentalTime::class) - private fun decryptAndVerify(receivedIntent: Intent? = null) { - if (api == null) { - bindToOpenKeychain(this) - return - } - val data = receivedIntent ?: Intent() - data.action = OpenPgpApi.ACTION_DECRYPT_VERIFY - - val inputStream = File(fullPath).inputStream() - val outputStream = ByteArrayOutputStream() - - lifecycleScope.launch(Dispatchers.Main) { - val result = - withContext(Dispatchers.IO) { - checkNotNull(api).executeApi(data, inputStream, outputStream) - } - when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - OpenPgpApi.RESULT_CODE_SUCCESS -> { - startAutoDismissTimer() - runCatching { - val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true) - val entry = passwordEntryFactory.create(outputStream.toByteArray()) - - if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) { - copyPasswordToClipboard(entry.password) - } - - passwordEntry = entry - invalidateOptionsMenu() - - val items = arrayListOf<FieldItem>() - if (!entry.password.isNullOrBlank()) { - items.add(FieldItem.createPasswordField(entry.password!!)) - } - - if (entry.hasTotp()) { - items.add(FieldItem.createOtpField(entry.totp.first())) - } - - if (!entry.username.isNullOrBlank()) { - items.add(FieldItem.createUsernameField(entry.username!!)) - } - - entry.extraContent.forEach { (key, value) -> - items.add(FieldItem(key, value, FieldItem.ActionType.COPY)) - } - - val adapter = - FieldItemAdapter(items, showPassword) { text -> copyTextToClipboard(text) } - binding.recyclerView.adapter = adapter - binding.recyclerView.itemAnimator = null - - if (entry.hasTotp()) { - entry.totp.onEach(adapter::updateOTPCode).launchIn(lifecycleScope) - } - } - .onFailure { e -> logcat(ERROR) { e.asLog() } } - } - OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - val sender = getUserInteractionRequestIntent(result) - userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) - } - OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/GetKeyIdsActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/GetKeyIdsActivity.kt deleted file mode 100644 index 2b25db0b..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/GetKeyIdsActivity.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.crypto - -import android.content.Intent -import android.os.Bundle -import androidx.activity.result.IntentSenderRequest -import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult -import androidx.lifecycle.lifecycleScope -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat -import me.msfjarvis.openpgpktx.util.OpenPgpApi -import me.msfjarvis.openpgpktx.util.OpenPgpUtils -import org.openintents.openpgp.IOpenPgpService2 - -class GetKeyIdsActivity : BasePgpActivity() { - - private val userInteractionRequiredResult = - registerForActivityResult(StartIntentSenderForResult()) { result -> - if (result.data == null || result.resultCode == RESULT_CANCELED) { - setResult(RESULT_CANCELED, result.data) - finish() - return@registerForActivityResult - } - getKeyIds(result.data!!) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - bindToOpenKeychain(this) - } - - override fun onBound(service: IOpenPgpService2) { - super.onBound(service) - getKeyIds() - } - - override fun onError(e: Exception) { - logcat(ERROR) { e.asLog() } - } - - /** Get the Key ids from OpenKeychain */ - private fun getKeyIds(data: Intent = Intent()) { - data.action = OpenPgpApi.ACTION_GET_KEY_IDS - lifecycleScope.launch(Dispatchers.Main) { - val result = withContext(Dispatchers.IO) { checkNotNull(api).executeApi(data, null, null) } - when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - OpenPgpApi.RESULT_CODE_SUCCESS -> { - runCatching { - val ids = - result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)?.map { - OpenPgpUtils.convertKeyIdToHex(it) - } - ?: emptyList() - val keyResult = Intent().putExtra(OpenPgpApi.EXTRA_KEY_IDS, ids.toTypedArray()) - setResult(RESULT_OK, keyResult) - finish() - } - .onFailure { e -> logcat(ERROR) { e.asLog() } } - } - OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - val sender = getUserInteractionRequestIntent(result) - userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) - } - OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt deleted file mode 100644 index f432227e..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt +++ /dev/null @@ -1,617 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.crypto - -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.ImageDecoder -import android.os.Build -import android.os.Bundle -import android.provider.MediaStore -import android.text.InputType -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.activity.result.IntentSenderRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult -import androidx.core.content.edit -import androidx.core.view.isVisible -import androidx.core.widget.doAfterTextChanged -import androidx.lifecycle.lifecycleScope -import app.passwordstore.R -import app.passwordstore.data.passfile.PasswordEntry -import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.databinding.PasswordCreationActivityBinding -import app.passwordstore.ui.dialogs.DicewarePasswordGeneratorDialogFragment -import app.passwordstore.ui.dialogs.OtpImportDialogFragment -import app.passwordstore.ui.dialogs.PasswordGeneratorDialogFragment -import app.passwordstore.util.autofill.AutofillPreferences -import app.passwordstore.util.autofill.DirectoryStructure -import app.passwordstore.util.crypto.GpgIdentifier -import app.passwordstore.util.extensions.asLog -import app.passwordstore.util.extensions.base64 -import app.passwordstore.util.extensions.commitChange -import app.passwordstore.util.extensions.getString -import app.passwordstore.util.extensions.isInsideRepository -import app.passwordstore.util.extensions.snackbar -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.settings.PreferenceKeys -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.onSuccess -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import com.google.zxing.BinaryBitmap -import com.google.zxing.LuminanceSource -import com.google.zxing.RGBLuminanceSource -import com.google.zxing.common.HybridBinarizer -import com.google.zxing.integration.android.IntentIntegrator -import com.google.zxing.integration.android.IntentIntegrator.QR_CODE -import com.google.zxing.qrcode.QRCodeReader -import dagger.hilt.android.AndroidEntryPoint -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.IOException -import javax.inject.Inject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat -import me.msfjarvis.openpgpktx.util.OpenPgpApi -import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection - -@AndroidEntryPoint -class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound { - - private val binding by viewBinding(PasswordCreationActivityBinding::inflate) - @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - - private val suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) } - private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) } - private val suggestedExtra by unsafeLazy { intent.getStringExtra(EXTRA_EXTRA_CONTENT) } - private val shouldGeneratePassword by unsafeLazy { - intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) - } - private val editing by unsafeLazy { intent.getBooleanExtra(EXTRA_EDITING, false) } - private val oldFileName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) } - private var oldCategory: String? = null - private var copy: Boolean = false - private var encryptionIntent: Intent = Intent() - - private val userInteractionRequiredResult = - registerForActivityResult(StartIntentSenderForResult()) { result -> - if (result.data == null) { - setResult(RESULT_CANCELED, null) - finish() - return@registerForActivityResult - } - - when (result.resultCode) { - RESULT_OK -> encrypt(result.data) - RESULT_CANCELED -> { - setResult(RESULT_CANCELED, result.data) - finish() - } - } - } - - private val otpImportAction = - registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - binding.otpImportButton.isVisible = false - val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data) - val contents = "${intentResult.contents}\n" - val currentExtras = binding.extraContent.text.toString() - if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') - binding.extraContent.append("\n$contents") - else binding.extraContent.append(contents) - snackbar(message = getString(R.string.otp_import_success)) - } else { - snackbar(message = getString(R.string.otp_import_failure)) - } - } - - private val imageImportAction = - registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri -> - if (imageUri == null) { - snackbar(message = getString(R.string.otp_import_failure)) - return@registerForActivityResult - } - val bitmap = - if (Build.VERSION.SDK_INT >= 28) { - ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver, imageUri)) - .copy(Bitmap.Config.ARGB_8888, true) - } else { - @Suppress("DEPRECATION") MediaStore.Images.Media.getBitmap(contentResolver, imageUri) - } - val intArray = IntArray(bitmap.width * bitmap.height) - // copy pixel data from the Bitmap into the 'intArray' array - bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) - val source: LuminanceSource = RGBLuminanceSource(bitmap.width, bitmap.height, intArray) - val binaryBitmap = BinaryBitmap(HybridBinarizer(source)) - - val reader = QRCodeReader() - runCatching { - val result = reader.decode(binaryBitmap) - val text = result.text - val currentExtras = binding.extraContent.text.toString() - if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') - binding.extraContent.append("\n$text") - else binding.extraContent.append(text) - snackbar(message = getString(R.string.otp_import_success)) - binding.otpImportButton.isVisible = false - } - .onFailure { snackbar(message = getString(R.string.otp_import_failure)) } - } - - private val gpgKeySelectAction = - registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> - lifecycleScope.launch { - val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id") - withContext(Dispatchers.IO) { - gpgIdentifierFile.writeText((keyIds + "").joinToString("\n")) - } - commitChange( - getString( - R.string.git_commit_gpg_id, - getLongName( - gpgIdentifierFile.parentFile!!.absolutePath, - repoPath, - gpgIdentifierFile.name - ) - ) - ) - .onSuccess { encrypt(encryptionIntent) } - } - } - } else { - snackbar( - message = getString(R.string.gpg_key_select_mandatory), - length = Snackbar.LENGTH_LONG - ) - } - } - - private fun File.findTillRoot(fileName: String, rootPath: File): File? { - val gpgFile = File(this, fileName) - if (gpgFile.exists()) return gpgFile - - if (this.absolutePath == rootPath.absolutePath) { - return null - } - - val parent = parentFile - return if (parent != null && parent.exists()) { - parent.findTillRoot(fileName, rootPath) - } else { - null - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - bindToOpenKeychain(this) - title = - if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title) - with(binding) { - setContentView(root) - generatePassword.setOnClickListener { generatePassword() } - otpImportButton.setOnClickListener { - supportFragmentManager.setFragmentResultListener( - OTP_RESULT_REQUEST_KEY, - this@PasswordCreationActivity - ) { requestKey, bundle -> - if (requestKey == OTP_RESULT_REQUEST_KEY) { - val contents = bundle.getString(RESULT) - val currentExtras = binding.extraContent.text.toString() - if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') - binding.extraContent.append("\n$contents") - else binding.extraContent.append(contents) - } - } - val hasCamera = packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) == true - if (hasCamera) { - val items = - arrayOf( - getString(R.string.otp_import_qr_code), - getString(R.string.otp_import_from_file), - getString(R.string.otp_import_manual_entry), - ) - MaterialAlertDialogBuilder(this@PasswordCreationActivity) - .setItems(items) { _, index -> - when (index) { - 0 -> - otpImportAction.launch( - IntentIntegrator(this@PasswordCreationActivity) - .setOrientationLocked(false) - .setBeepEnabled(false) - .setDesiredBarcodeFormats(QR_CODE) - .createScanIntent() - ) - 1 -> imageImportAction.launch("image/*") - 2 -> OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") - } - } - .show() - } else { - OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") - } - } - - directoryInputLayout.apply { - if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) { - isEnabled = true - } else { - setBackgroundColor(getColor(android.R.color.transparent)) - } - val path = getRelativePath(fullPath, repoPath) - // Keep empty path field visible if it is editable. - if (path.isEmpty() && !isEnabled) visibility = View.GONE - else { - directory.setText(path) - oldCategory = path - } - } - if (suggestedName != null) { - filename.setText(suggestedName) - } else { - filename.requestFocus() - } - // Allow the user to quickly switch between storing the username as the filename or - // in the encrypted extras. This only makes sense if the directory structure is - // FileBased. - if ( - suggestedName == null && - AutofillPreferences.directoryStructure(this@PasswordCreationActivity) == - DirectoryStructure.FileBased - ) { - encryptUsername.apply { - visibility = View.VISIBLE - setOnClickListener { - if (isChecked) { - // User wants to enable username encryption, so we add it to the - // encrypted extras as the first line. - val username = filename.text.toString() - val extras = "username:$username\n${extraContent.text}" - - filename.text?.clear() - extraContent.setText(extras) - } else { - // User wants to disable username encryption, so we extract the - // username from the encrypted extras and use it as the filename. - val entry = - passwordEntryFactory.create("PASSWORD\n${extraContent.text}".encodeToByteArray()) - val username = entry.username - - // username should not be null here by the logic in - // updateViewState, but it could still happen due to - // input lag. - if (username != null) { - filename.setText(username) - extraContent.setText(entry.extraContentWithoutAuthData) - } - } - } - } - } - suggestedPass?.let { - password.setText(it) - password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - } - suggestedExtra?.let { extraContent.setText(it) } - if (shouldGeneratePassword) { - generatePassword() - password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - } - } - listOf(binding.filename, binding.extraContent).forEach { - it.doAfterTextChanged { updateViewState() } - } - updateViewState() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.pgp_handler_new_password, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - setResult(RESULT_CANCELED) - onBackPressed() - } - R.id.save_password -> { - copy = false - encrypt() - } - R.id.save_and_copy_password -> { - copy = true - encrypt() - } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - private fun generatePassword() { - supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) { - requestKey, - bundle -> - if (requestKey == PASSWORD_RESULT_REQUEST_KEY) { - binding.password.setText(bundle.getString(RESULT)) - } - } - when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) { - KEY_PWGEN_TYPE_CLASSIC -> - PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator") - KEY_PWGEN_TYPE_DICEWARE -> - DicewarePasswordGeneratorDialogFragment().show(supportFragmentManager, "generator") - } - } - - private fun updateViewState() = - with(binding) { - // Use PasswordEntry to parse extras for username - val entry = - passwordEntryFactory.create("PLACEHOLDER\n${extraContent.text}".encodeToByteArray()) - encryptUsername.apply { - if (visibility != View.VISIBLE) return@apply - val hasUsernameInFileName = filename.text.toString().isNotBlank() - val hasUsernameInExtras = !entry.username.isNullOrBlank() - isEnabled = hasUsernameInFileName xor hasUsernameInExtras - isChecked = hasUsernameInExtras - } - otpImportButton.isVisible = !entry.hasTotp() - } - - /** Encrypts the password and the extra content */ - private fun encrypt(receivedIntent: Intent? = null) { - with(binding) { - val editName = filename.text.toString().trim() - val editPass = password.text.toString() - val editExtra = extraContent.text.toString() - - if (editName.isEmpty()) { - snackbar(message = resources.getString(R.string.file_toast_text)) - return@with - } else if (editName.contains('/')) { - snackbar(message = resources.getString(R.string.invalid_filename_text)) - return@with - } - - if (editPass.isEmpty() && editExtra.isEmpty()) { - snackbar(message = resources.getString(R.string.empty_toast_text)) - return@with - } - - if (copy) { - copyPasswordToClipboard(editPass) - } - - encryptionIntent = receivedIntent ?: Intent() - encryptionIntent.action = OpenPgpApi.ACTION_ENCRYPT - - // pass enters the key ID into `.gpg-id`. - val repoRoot = PasswordRepository.getRepositoryDirectory() - val gpgIdentifierFile = - File(repoRoot, directory.text.toString()).findTillRoot(".gpg-id", repoRoot) - ?: File(repoRoot, ".gpg-id").apply { createNewFile() } - val gpgIdentifiers = - gpgIdentifierFile - .readLines() - .filter { it.isNotBlank() } - .map { line -> - GpgIdentifier.fromString(line) - ?: run { - // The line being empty means this is most likely an empty `.gpg-id` - // file we created. Skip the validation so we can make the user add a - // real ID. - if (line.isEmpty()) return@run - if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex())) { - snackbar(message = resources.getString(R.string.short_key_ids_unsupported)) - } else { - snackbar(message = resources.getString(R.string.invalid_gpg_id)) - } - return@with - } - } - if (gpgIdentifiers.isEmpty()) { - gpgKeySelectAction.launch( - Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java) - ) - return@with - } - val keyIds = - gpgIdentifiers.filterIsInstance<GpgIdentifier.KeyId>().map { it.id }.toLongArray() - if (keyIds.isNotEmpty()) { - encryptionIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIds) - } - val userIds = - gpgIdentifiers.filterIsInstance<GpgIdentifier.UserId>().map { it.email }.toTypedArray() - if (userIds.isNotEmpty()) { - encryptionIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds) - } - - encryptionIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, false) - - val content = "$editPass\n$editExtra" - val inputStream = ByteArrayInputStream(content.toByteArray()) - val outputStream = ByteArrayOutputStream() - - val path = - when { - // If we allowed the user to edit the relative path, we have to consider it here - // instead - // of fullPath. - directoryInputLayout.isEnabled -> { - val editRelativePath = directory.text.toString().trim() - if (editRelativePath.isEmpty()) { - snackbar(message = resources.getString(R.string.path_toast_text)) - return - } - val passwordDirectory = File("$repoPath/${editRelativePath.trim('/')}") - if (!passwordDirectory.exists() && !passwordDirectory.mkdir()) { - snackbar(message = "Failed to create directory ${editRelativePath.trim('/')}") - return - } - - "${passwordDirectory.path}/$editName.gpg" - } - else -> "$fullPath/$editName.gpg" - } - - lifecycleScope.launch(Dispatchers.Main) { - val result = - withContext(Dispatchers.IO) { - checkNotNull(api).executeApi(encryptionIntent, inputStream, outputStream) - } - when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - OpenPgpApi.RESULT_CODE_SUCCESS -> { - runCatching { - val file = File(path) - // If we're not editing, this file should not already exist! - // Additionally, if we were editing and the incoming and outgoing - // filenames differ, it means we renamed. Ensure that the target - // doesn't already exist to prevent an accidental overwrite. - if ( - (!editing || (editing && suggestedName != file.nameWithoutExtension)) && - file.exists() - ) { - snackbar(message = getString(R.string.password_creation_duplicate_error)) - return@runCatching - } - - if (!file.isInsideRepository()) { - snackbar(message = getString(R.string.message_error_destination_outside_repo)) - return@runCatching - } - - withContext(Dispatchers.IO) { - file.outputStream().use { it.write(outputStream.toByteArray()) } - } - - // associate the new password name with the last name's timestamp in - // history - val preference = - getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) - val oldFilePathHash = - "$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg".base64() - val timestamp = preference.getString(oldFilePathHash) - if (timestamp != null) { - preference.edit { - remove(oldFilePathHash) - putString(file.absolutePath.base64(), timestamp) - } - } - - val returnIntent = Intent() - returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path) - returnIntent.putExtra(RETURN_EXTRA_NAME, editName) - returnIntent.putExtra( - RETURN_EXTRA_LONG_NAME, - getLongName(fullPath, repoPath, editName) - ) - - if (shouldGeneratePassword) { - val directoryStructure = - AutofillPreferences.directoryStructure(applicationContext) - val entry = passwordEntryFactory.create(content.encodeToByteArray()) - returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password) - val username = entry.username ?: directoryStructure.getUsernameFor(file) - returnIntent.putExtra(RETURN_EXTRA_USERNAME, username) - } - - if ( - directoryInputLayout.isVisible && - directoryInputLayout.isEnabled && - oldFileName != null - ) { - val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg") - if (oldFile.path != file.path && !oldFile.delete()) { - setResult(RESULT_CANCELED) - MaterialAlertDialogBuilder(this@PasswordCreationActivity) - .setTitle(R.string.password_creation_file_fail_title) - .setMessage( - getString(R.string.password_creation_file_delete_fail_message, oldFileName) - ) - .setCancelable(false) - .setPositiveButton(android.R.string.ok) { _, _ -> finish() } - .show() - return@runCatching - } - } - - val commitMessageRes = - if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text - lifecycleScope.launch { - commitChange( - resources.getString( - commitMessageRes, - getLongName(fullPath, repoPath, editName) - ) - ) - .onSuccess { - setResult(RESULT_OK, returnIntent) - finish() - } - } - } - .onFailure { e -> - if (e is IOException) { - logcat(ERROR) { e.asLog("Failed to write password file") } - setResult(RESULT_CANCELED) - MaterialAlertDialogBuilder(this@PasswordCreationActivity) - .setTitle(getString(R.string.password_creation_file_fail_title)) - .setMessage(getString(R.string.password_creation_file_write_fail_message)) - .setCancelable(false) - .setPositiveButton(android.R.string.ok) { _, _ -> finish() } - .show() - } else { - logcat(ERROR) { e.asLog() } - } - } - } - OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - val sender = getUserInteractionRequestIntent(result) - userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) - } - OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) - } - } - } - } - - companion object { - - private const val KEY_PWGEN_TYPE_CLASSIC = "classic" - private const val KEY_PWGEN_TYPE_DICEWARE = "diceware" - const val PASSWORD_RESULT_REQUEST_KEY = "PASSWORD_GENERATOR" - const val OTP_RESULT_REQUEST_KEY = "OTP_IMPORT" - const val RESULT = "RESULT" - const val RETURN_EXTRA_CREATED_FILE = "CREATED_FILE" - const val RETURN_EXTRA_NAME = "NAME" - const val RETURN_EXTRA_LONG_NAME = "LONG_NAME" - const val RETURN_EXTRA_USERNAME = "USERNAME" - const val RETURN_EXTRA_PASSWORD = "PASSWORD" - const val EXTRA_FILE_NAME = "FILENAME" - const val EXTRA_PASSWORD = "PASSWORD" - const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT" - const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD" - const val EXTRA_EDITING = "EDITING" - } -} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivityV2.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivityV2.kt index 7070ce7b..50a6e825 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivityV2.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivityV2.kt @@ -134,7 +134,6 @@ class PasswordCreationActivityV2 : BasePgpActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportActionBar?.setDisplayHomeAsUpEnabled(true) - bindToOpenKeychain(this) title = if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title) with(binding) { diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt index bb3b6f6f..04488e90 100644 --- a/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt @@ -19,7 +19,7 @@ import app.passwordstore.R import app.passwordstore.databinding.FragmentPwgenDicewareBinding import app.passwordstore.injection.prefs.PasswordGeneratorPreferences import app.passwordstore.passgen.diceware.DicewarePassphraseGenerator -import app.passwordstore.ui.crypto.PasswordCreationActivity +import app.passwordstore.ui.crypto.PasswordCreationActivityV2 import app.passwordstore.util.extensions.getString import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_LENGTH import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_SEPARATOR @@ -58,8 +58,8 @@ class DicewarePasswordGeneratorDialogFragment : DialogFragment() { setTitle(R.string.pwgen_title) setPositiveButton(R.string.dialog_ok) { _, _ -> setFragmentResult( - PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY, - bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}") + PasswordCreationActivityV2.PASSWORD_RESULT_REQUEST_KEY, + bundleOf(PasswordCreationActivityV2.RESULT to "${binding.passwordText.text}") ) } setNeutralButton(R.string.dialog_cancel) { _, _ -> } diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/FolderCreationDialogFragment.kt index 78177f10..cac3bcd7 100644 --- a/app/src/main/java/app/passwordstore/ui/dialogs/FolderCreationDialogFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/dialogs/FolderCreationDialogFragment.kt @@ -5,60 +5,21 @@ package app.passwordstore.ui.dialogs import android.app.Dialog -import android.content.Intent import android.os.Bundle import android.view.WindowManager -import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment -import androidx.lifecycle.lifecycleScope import app.passwordstore.R -import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.ui.crypto.BasePgpActivity -import app.passwordstore.ui.crypto.GetKeyIdsActivity import app.passwordstore.ui.passwords.PasswordStore -import app.passwordstore.util.extensions.commitChange -import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import java.io.File -import kotlinx.coroutines.launch -import me.msfjarvis.openpgpktx.util.OpenPgpApi class FolderCreationDialogFragment : DialogFragment() { private lateinit var newFolder: File - - private val keySelectAction = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> - val gpgIdentifierFile = File(newFolder, ".gpg-id") - gpgIdentifierFile.writeText(keyIds.joinToString("\n")) - if (PasswordRepository.repository != null) { - lifecycleScope.launch { - val repoPath = PasswordRepository.getRepositoryDirectory().absolutePath - requireActivity() - .commitChange( - getString( - R.string.git_commit_gpg_id, - BasePgpActivity.getLongName( - gpgIdentifierFile.parentFile!!.absolutePath, - repoPath, - gpgIdentifierFile.name - ) - ), - ) - dismiss() - } - } - } - } - } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext()) alertDialogBuilder.setTitle(R.string.title_create_folder) @@ -89,12 +50,16 @@ class FolderCreationDialogFragment : DialogFragment() { if (folderNameViewContainer.error != null) return newFolder.mkdirs() (requireActivity() as PasswordStore).refreshPasswordList(newFolder) - if (dialog.findViewById<MaterialCheckBox>(R.id.set_gpg_key).isChecked) { - keySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) - return - } else { - dismiss() - } + // TODO(msfjarvis): Restore this functionality + /* + if (dialog.findViewById<MaterialCheckBox>(R.id.set_gpg_key).isChecked) { + keySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) + return + } else { + dismiss() + } + */ + dismiss() } companion object { diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt index 4d2413d4..cd36be04 100644 --- a/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt @@ -13,7 +13,7 @@ import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import androidx.fragment.app.setFragmentResult import app.passwordstore.databinding.FragmentManualOtpEntryBinding -import app.passwordstore.ui.crypto.PasswordCreationActivity +import app.passwordstore.ui.crypto.PasswordCreationActivityV2 import com.google.android.material.dialog.MaterialAlertDialogBuilder class OtpImportDialogFragment : DialogFragment() { @@ -24,8 +24,8 @@ class OtpImportDialogFragment : DialogFragment() { builder.setView(binding.root) builder.setPositiveButton(android.R.string.ok) { _, _ -> setFragmentResult( - PasswordCreationActivity.OTP_RESULT_REQUEST_KEY, - bundleOf(PasswordCreationActivity.RESULT to getTOTPUri(binding)) + PasswordCreationActivityV2.OTP_RESULT_REQUEST_KEY, + bundleOf(PasswordCreationActivityV2.RESULT to getTOTPUri(binding)) ) } val dialog = builder.create() diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt index 06ce2d92..352c755f 100644 --- a/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt @@ -26,7 +26,7 @@ import app.passwordstore.passgen.random.NoCharactersIncludedException import app.passwordstore.passgen.random.PasswordGenerator import app.passwordstore.passgen.random.PasswordLengthTooShortException import app.passwordstore.passgen.random.PasswordOption -import app.passwordstore.ui.crypto.PasswordCreationActivity +import app.passwordstore.ui.crypto.PasswordCreationActivityV2 import app.passwordstore.util.settings.PreferenceKeys import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.runCatching @@ -72,8 +72,8 @@ class PasswordGeneratorDialogFragment : DialogFragment() { setTitle(R.string.pwgen_title) setPositiveButton(R.string.dialog_ok) { _, _ -> setFragmentResult( - PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY, - bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}") + PasswordCreationActivityV2.PASSWORD_RESULT_REQUEST_KEY, + bundleOf(PasswordCreationActivityV2.RESULT to "${binding.passwordText.text}") ) } setNeutralButton(R.string.dialog_cancel) { _, _ -> } diff --git a/app/src/main/java/app/passwordstore/ui/git/base/BaseGitActivity.kt b/app/src/main/java/app/passwordstore/ui/git/base/BaseGitActivity.kt index fb9cfde3..b79a1425 100644 --- a/app/src/main/java/app/passwordstore/ui/git/base/BaseGitActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/git/base/BaseGitActivity.kt @@ -18,7 +18,6 @@ import app.passwordstore.util.git.operation.PullOperation import app.passwordstore.util.git.operation.PushOperation import app.passwordstore.util.git.operation.ResetToRemoteOperation import app.passwordstore.util.git.operation.SyncOperation -import app.passwordstore.util.git.sshj.ContinuationContainerActivity import app.passwordstore.util.settings.GitSettings import app.passwordstore.util.settings.PreferenceKeys import com.github.michaelbull.result.Err @@ -42,7 +41,7 @@ import net.schmizz.sshj.userauth.UserAuthException * git-related tasks and makes sense to be held here. */ @AndroidEntryPoint -abstract class BaseGitActivity : ContinuationContainerActivity() { +abstract class BaseGitActivity : AppCompatActivity() { /** Enum of possible Git operations than can be run through [launchGitOperation]. */ enum class GitOp { diff --git a/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt b/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt index 9cac0fdd..a7ecd697 100644 --- a/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt @@ -62,7 +62,6 @@ class GitServerConfigActivity : BaseGitActivity() { when (newAuthMode) { AuthMode.SshKey -> check(binding.authModeSshKey.id) AuthMode.Password -> check(binding.authModePassword.id) - AuthMode.OpenKeychain -> check(binding.authModeOpenKeychain.id) AuthMode.None -> check(View.NO_ID) } addOnButtonCheckedListener { _, checkedId, isChecked -> @@ -72,7 +71,6 @@ class GitServerConfigActivity : BaseGitActivity() { } when (checkedId) { binding.authModeSshKey.id -> newAuthMode = AuthMode.SshKey - binding.authModeOpenKeychain.id -> newAuthMode = AuthMode.OpenKeychain binding.authModePassword.id -> newAuthMode = AuthMode.Password View.NO_ID -> newAuthMode = AuthMode.None } @@ -215,12 +213,10 @@ class GitServerConfigActivity : BaseGitActivity() { with(binding) { if (isHttps) { authModeSshKey.isVisible = false - authModeOpenKeychain.isVisible = false authModePassword.isVisible = true if (authModeGroup.checkedButtonId != authModePassword.id) authModeGroup.check(View.NO_ID) } else { authModeSshKey.isVisible = true - authModeOpenKeychain.isVisible = true authModePassword.isVisible = true if (authModeGroup.checkedButtonId == View.NO_ID) authModeGroup.check(authModeSshKey.id) } diff --git a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt index d2a34c80..1a2e1a60 100644 --- a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt @@ -11,13 +11,11 @@ import android.os.Looper import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit import app.passwordstore.ui.crypto.BasePgpActivity -import app.passwordstore.ui.crypto.DecryptActivity import app.passwordstore.ui.crypto.DecryptActivityV2 import app.passwordstore.ui.passwords.PasswordStore import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.auth.BiometricAuthenticator.Result import app.passwordstore.util.extensions.sharedPrefs -import app.passwordstore.util.features.Feature import app.passwordstore.util.features.Features import app.passwordstore.util.settings.PreferenceKeys import dagger.hilt.android.AndroidEntryPoint diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt index 2b32cb61..43dc7f4c 100644 --- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt @@ -5,65 +5,21 @@ package app.passwordstore.ui.onboarding.fragments -import android.content.Intent import android.os.Bundle import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.edit import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import app.passwordstore.R -import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.databinding.FragmentKeySelectionBinding -import app.passwordstore.ui.crypto.GetKeyIdsActivity -import app.passwordstore.util.extensions.commitChange -import app.passwordstore.util.extensions.finish -import app.passwordstore.util.extensions.sharedPrefs -import app.passwordstore.util.extensions.snackbar -import app.passwordstore.util.extensions.unsafeLazy import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.settings.PreferenceKeys -import com.google.android.material.snackbar.Snackbar -import java.io.File -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import me.msfjarvis.openpgpktx.util.OpenPgpApi class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) { - private val settings by unsafeLazy { requireActivity().applicationContext.sharedPrefs } private val binding by viewBinding(FragmentKeySelectionBinding::bind) - - private val gpgKeySelectAction = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> - lifecycleScope.launch { - withContext(Dispatchers.IO) { - val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id") - gpgIdentifierFile.writeText((keyIds + "").joinToString("\n")) - } - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - requireActivity() - .commitChange(getString(R.string.git_commit_gpg_id, getString(R.string.app_name))) - } - } - finish() - } else { - requireActivity() - .snackbar( - message = getString(R.string.gpg_key_select_mandatory), - length = Snackbar.LENGTH_LONG - ) - } - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.selectKey.setOnClickListener { - gpgKeySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) + // TODO(msfjarvis): Restore this functionality + // gpgKeySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) } } diff --git a/app/src/main/java/app/passwordstore/util/crypto/GpgIdentifier.kt b/app/src/main/java/app/passwordstore/util/crypto/GpgIdentifier.kt deleted file mode 100644 index 3df3e09f..00000000 --- a/app/src/main/java/app/passwordstore/util/crypto/GpgIdentifier.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.crypto - -import me.msfjarvis.openpgpktx.util.OpenPgpUtils - -sealed class GpgIdentifier { - data class KeyId(val id: Long) : GpgIdentifier() - data class UserId(val email: String) : GpgIdentifier() - - companion object { - @OptIn(ExperimentalUnsignedTypes::class) - fun fromString(identifier: String): GpgIdentifier? { - if (identifier.isEmpty()) return null - // Match long key IDs: - // FF22334455667788 or 0xFF22334455667788 - val maybeLongKeyId = - identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{16}".toRegex()) } - if (maybeLongKeyId != null) { - val keyId = maybeLongKeyId.toULong(16) - return KeyId(keyId.toLong()) - } - - // Match fingerprints: - // FF223344556677889900112233445566778899 or 0xFF223344556677889900112233445566778899 - val maybeFingerprint = - identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{40}".toRegex()) } - if (maybeFingerprint != null) { - // Truncating to the long key ID is not a security issue since OpenKeychain only - // accepts - // non-ambiguous key IDs. - val keyId = maybeFingerprint.takeLast(16).toULong(16) - return KeyId(keyId.toLong()) - } - - return OpenPgpUtils.splitUserId(identifier).email?.let { UserId(it) } - } - } -} diff --git a/app/src/main/java/app/passwordstore/util/git/operation/BreakOutOfDetached.kt b/app/src/main/java/app/passwordstore/util/git/operation/BreakOutOfDetached.kt index 65629daa..2ce6f625 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/BreakOutOfDetached.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/BreakOutOfDetached.kt @@ -4,16 +4,15 @@ */ package app.passwordstore.util.git.operation +import androidx.appcompat.app.AppCompatActivity import app.passwordstore.R import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.git.sshj.ContinuationContainerActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.eclipse.jgit.api.RebaseCommand import org.eclipse.jgit.api.ResetCommand import org.eclipse.jgit.lib.RepositoryState -class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) : - GitOperation(callingActivity) { +class BreakOutOfDetached(callingActivity: AppCompatActivity) : GitOperation(callingActivity) { private val merging = repository.repositoryState == RepositoryState.MERGING private val resetCommands = diff --git a/app/src/main/java/app/passwordstore/util/git/operation/CloneOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/CloneOperation.kt index 88178125..f0348771 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/CloneOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/CloneOperation.kt @@ -4,7 +4,7 @@ */ package app.passwordstore.util.git.operation -import app.passwordstore.util.git.sshj.ContinuationContainerActivity +import androidx.appcompat.app.AppCompatActivity import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.GitCommand @@ -14,7 +14,7 @@ import org.eclipse.jgit.api.GitCommand * @param uri URL to clone the repository from * @param callingActivity the calling activity */ -class CloneOperation(callingActivity: ContinuationContainerActivity, uri: String) : +class CloneOperation(callingActivity: AppCompatActivity, uri: String) : GitOperation(callingActivity) { override val commands: Array<GitCommand<out Any>> = diff --git a/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt index 556c899e..69535fc3 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt @@ -5,7 +5,7 @@ package app.passwordstore.util.git.operation -import app.passwordstore.util.git.sshj.ContinuationContainerActivity +import androidx.appcompat.app.AppCompatActivity import org.eclipse.jgit.api.GitCommand /** @@ -13,7 +13,7 @@ import org.eclipse.jgit.api.GitCommand * achieve the best compression. */ class GcOperation( - callingActivity: ContinuationContainerActivity, + callingActivity: AppCompatActivity, ) : GitOperation(callingActivity) { override val requiresAuth: Boolean = false diff --git a/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt index 2f13b5ad..02cea621 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt @@ -6,6 +6,7 @@ package app.passwordstore.util.git.operation import android.content.Intent import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity import app.passwordstore.R import app.passwordstore.data.repo.PasswordRepository @@ -14,7 +15,6 @@ import app.passwordstore.ui.sshkeygen.SshKeyImportActivity import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.auth.BiometricAuthenticator.Result.* import app.passwordstore.util.git.GitCommandExecutor -import app.passwordstore.util.git.sshj.ContinuationContainerActivity import app.passwordstore.util.git.sshj.SshAuthMethod import app.passwordstore.util.git.sshj.SshKey import app.passwordstore.util.git.sshj.SshjSessionFactory @@ -74,7 +74,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { protected val git = Git(repository) protected val remoteBranch = hiltEntryPoint.gitSettings().branch private val authActivity - get() = callingActivity as ContinuationContainerActivity + get() = callingActivity as AppCompatActivity private class HttpsCredentialsProvider(private val passwordFinder: PasswordFinder) : CredentialsProvider() { @@ -213,7 +213,6 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { // error, allowing users to make the SSH key selection. return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER)) } - AuthMode.OpenKeychain -> registerAuthProviders(SshAuthMethod.OpenKeychain(authActivity)) AuthMode.Password -> { val httpsCredentialProvider = HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password)) diff --git a/app/src/main/java/app/passwordstore/util/git/operation/PullOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/PullOperation.kt index 75b6fc1a..a211a2f7 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/PullOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/PullOperation.kt @@ -4,11 +4,11 @@ */ package app.passwordstore.util.git.operation -import app.passwordstore.util.git.sshj.ContinuationContainerActivity +import androidx.appcompat.app.AppCompatActivity import org.eclipse.jgit.api.GitCommand class PullOperation( - callingActivity: ContinuationContainerActivity, + callingActivity: AppCompatActivity, rebase: Boolean, ) : GitOperation(callingActivity) { diff --git a/app/src/main/java/app/passwordstore/util/git/operation/PushOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/PushOperation.kt index 386d79e6..fa2dd1ba 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/PushOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/PushOperation.kt @@ -4,11 +4,10 @@ */ package app.passwordstore.util.git.operation -import app.passwordstore.util.git.sshj.ContinuationContainerActivity +import androidx.appcompat.app.AppCompatActivity import org.eclipse.jgit.api.GitCommand -class PushOperation(callingActivity: ContinuationContainerActivity) : - GitOperation(callingActivity) { +class PushOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) { override val commands: Array<GitCommand<out Any>> = arrayOf( diff --git a/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt index 7c8cee93..ddbdc807 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt @@ -4,11 +4,10 @@ */ package app.passwordstore.util.git.operation -import app.passwordstore.util.git.sshj.ContinuationContainerActivity +import androidx.appcompat.app.AppCompatActivity import org.eclipse.jgit.api.ResetCommand -class ResetToRemoteOperation(callingActivity: ContinuationContainerActivity) : - GitOperation(callingActivity) { +class ResetToRemoteOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) { override val commands = arrayOf( diff --git a/app/src/main/java/app/passwordstore/util/git/operation/SyncOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/SyncOperation.kt index 226fd753..e36b01b1 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/SyncOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/SyncOperation.kt @@ -4,10 +4,10 @@ */ package app.passwordstore.util.git.operation -import app.passwordstore.util.git.sshj.ContinuationContainerActivity +import androidx.appcompat.app.AppCompatActivity class SyncOperation( - callingActivity: ContinuationContainerActivity, + callingActivity: AppCompatActivity, rebase: Boolean, ) : GitOperation(callingActivity) { diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/ContinuationContainerActivity.kt b/app/src/main/java/app/passwordstore/util/git/sshj/ContinuationContainerActivity.kt deleted file mode 100644 index 10872a24..00000000 --- a/app/src/main/java/app/passwordstore/util/git/sshj/ContinuationContainerActivity.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package app.passwordstore.util.git.sshj - -import android.content.Intent -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.LayoutRes -import androidx.appcompat.app.AppCompatActivity -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import net.schmizz.sshj.common.DisconnectReason -import net.schmizz.sshj.userauth.UserAuthException - -/** Workaround for https://msfjarvis.dev/aps/issue/1164 */ -open class ContinuationContainerActivity : AppCompatActivity { - - constructor() : super() - constructor(@LayoutRes layoutRes: Int) : super(layoutRes) - - var stashedCont: Continuation<Intent>? = null - - val continueAfterUserInteraction = - registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result -> - stashedCont?.let { cont -> - stashedCont = null - val data = result.data - if (data != null) cont.resume(data) - else cont.resumeWithException(UserAuthException(DisconnectReason.AUTH_CANCELLED_BY_USER)) - } - } -} diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/OpenKeychainKeyProvider.kt b/app/src/main/java/app/passwordstore/util/git/sshj/OpenKeychainKeyProvider.kt deleted file mode 100644 index 7603059f..00000000 --- a/app/src/main/java/app/passwordstore/util/git/sshj/OpenKeychainKeyProvider.kt +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package app.passwordstore.util.git.sshj - -import android.app.PendingIntent -import android.content.Intent -import androidx.activity.result.IntentSenderRequest -import androidx.core.content.edit -import androidx.lifecycle.lifecycleScope -import app.passwordstore.util.extensions.OPENPGP_PROVIDER -import app.passwordstore.util.extensions.sharedPrefs -import app.passwordstore.util.settings.PreferenceKeys -import java.io.Closeable -import java.security.PublicKey -import java.security.interfaces.ECKey -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import logcat.logcat -import net.schmizz.sshj.common.DisconnectReason -import net.schmizz.sshj.common.KeyType -import net.schmizz.sshj.userauth.UserAuthException -import net.schmizz.sshj.userauth.keyprovider.KeyProvider -import org.openintents.ssh.authentication.ISshAuthenticationService -import org.openintents.ssh.authentication.SshAuthenticationApi -import org.openintents.ssh.authentication.SshAuthenticationApiError -import org.openintents.ssh.authentication.SshAuthenticationConnection -import org.openintents.ssh.authentication.request.KeySelectionRequest -import org.openintents.ssh.authentication.request.Request -import org.openintents.ssh.authentication.request.SigningRequest -import org.openintents.ssh.authentication.request.SshPublicKeyRequest -import org.openintents.ssh.authentication.response.KeySelectionResponse -import org.openintents.ssh.authentication.response.Response -import org.openintents.ssh.authentication.response.SigningResponse -import org.openintents.ssh.authentication.response.SshPublicKeyResponse - -class OpenKeychainKeyProvider private constructor(val activity: ContinuationContainerActivity) : - KeyProvider, Closeable { - - companion object { - - suspend fun prepareAndUse( - activity: ContinuationContainerActivity, - block: (provider: OpenKeychainKeyProvider) -> Unit - ) { - withContext(Dispatchers.Main) { OpenKeychainKeyProvider(activity) }.prepareAndUse(block) - } - } - - private sealed class ApiResponse { - data class Success(val response: Response) : ApiResponse() - data class GeneralError(val exception: Exception) : ApiResponse() - data class NoSuchKey(val exception: Exception) : ApiResponse() - } - - private val context = activity.applicationContext - private val sshServiceConnection = SshAuthenticationConnection(context, OPENPGP_PROVIDER) - private val preferences = context.sharedPrefs - private lateinit var sshServiceApi: SshAuthenticationApi - - private var keyId - get() = preferences.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null) - set(value) { - preferences.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, value) } - } - private var publicKey: PublicKey? = null - private var privateKey: OpenKeychainPrivateKey? = null - - private suspend fun prepareAndUse(block: (provider: OpenKeychainKeyProvider) -> Unit) { - prepare() - use(block) - } - - private suspend fun prepare() { - sshServiceApi = suspendCoroutine { cont -> - sshServiceConnection.connect( - object : SshAuthenticationConnection.OnBound { - override fun onBound(sshAgent: ISshAuthenticationService) { - logcat { "Bound to SshAuthenticationApi: $OPENPGP_PROVIDER" } - cont.resume(SshAuthenticationApi(context, sshAgent)) - } - - override fun onError() { - throw UserAuthException(DisconnectReason.UNKNOWN, "OpenKeychain service unavailable") - } - } - ) - } - - if (keyId == null) { - selectKey() - } - check(keyId != null) - fetchPublicKey() - makePrivateKey() - } - - private suspend fun fetchPublicKey(isRetry: Boolean = false) { - when (val sshPublicKeyResponse = executeApiRequest(SshPublicKeyRequest(keyId))) { - is ApiResponse.Success -> { - val response = sshPublicKeyResponse.response as SshPublicKeyResponse - val sshPublicKey = response.sshPublicKey!! - publicKey = - parseSshPublicKey(sshPublicKey) - ?: throw IllegalStateException("OpenKeychain API returned invalid SSH key") - } - is ApiResponse.NoSuchKey -> - if (isRetry) { - throw sshPublicKeyResponse.exception - } else { - // Allow the user to reselect an authentication key and retry - selectKey() - fetchPublicKey(true) - } - is ApiResponse.GeneralError -> throw sshPublicKeyResponse.exception - } - } - - private suspend fun selectKey() { - when (val keySelectionResponse = executeApiRequest(KeySelectionRequest())) { - is ApiResponse.Success -> - keyId = (keySelectionResponse.response as KeySelectionResponse).keyId - is ApiResponse.GeneralError -> throw keySelectionResponse.exception - is ApiResponse.NoSuchKey -> throw keySelectionResponse.exception - } - } - - private suspend fun executeApiRequest( - request: Request, - resultOfUserInteraction: Intent? = null - ): ApiResponse { - logcat { "executeRequest($request) called" } - val result = - withContext(Dispatchers.Main) { - // If the request required user interaction, the data returned from the - // PendingIntent - // is used as the real request. - sshServiceApi.executeApi(resultOfUserInteraction ?: request.toIntent())!! - } - return parseResult(request, result).also { logcat { "executeRequest($request): $it" } } - } - - private suspend fun parseResult(request: Request, result: Intent): ApiResponse { - return when ( - result.getIntExtra( - SshAuthenticationApi.EXTRA_RESULT_CODE, - SshAuthenticationApi.RESULT_CODE_ERROR - ) - ) { - SshAuthenticationApi.RESULT_CODE_SUCCESS -> { - ApiResponse.Success( - when (request) { - is KeySelectionRequest -> KeySelectionResponse(result) - is SshPublicKeyRequest -> SshPublicKeyResponse(result) - is SigningRequest -> SigningResponse(result) - else -> throw IllegalArgumentException("Unsupported OpenKeychain request type") - } - ) - } - SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - val pendingIntent: PendingIntent = - result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT)!! - val resultOfUserInteraction: Intent = - withContext(Dispatchers.Main) { - suspendCoroutine { cont -> - activity.stashedCont = cont - activity.continueAfterUserInteraction.launch( - IntentSenderRequest.Builder(pendingIntent).build() - ) - } - } - executeApiRequest(request, resultOfUserInteraction) - } - else -> { - val error = - result.getParcelableExtra<SshAuthenticationApiError>(SshAuthenticationApi.EXTRA_ERROR) - val exception = - UserAuthException( - DisconnectReason.UNKNOWN, - "Request ${request::class.simpleName} failed: ${error?.message}" - ) - when (error?.error) { - SshAuthenticationApiError.NO_AUTH_KEY, - SshAuthenticationApiError.NO_SUCH_KEY -> ApiResponse.NoSuchKey(exception) - else -> ApiResponse.GeneralError(exception) - } - } - } - } - - private fun makePrivateKey() { - check(keyId != null && publicKey != null) - privateKey = - object : OpenKeychainPrivateKey { - override suspend fun sign(challenge: ByteArray, hashAlgorithm: Int) = - when ( - val signingResponse = executeApiRequest(SigningRequest(challenge, keyId, hashAlgorithm)) - ) { - is ApiResponse.Success -> (signingResponse.response as SigningResponse).signature - is ApiResponse.GeneralError -> throw signingResponse.exception - is ApiResponse.NoSuchKey -> throw signingResponse.exception - } - - override fun getAlgorithm() = publicKey!!.algorithm - override fun getParams() = (publicKey as? ECKey)?.params - } - } - - override fun close() { - activity.lifecycleScope.launch { - withContext(Dispatchers.Main) { activity.continueAfterUserInteraction.unregister() } - } - sshServiceConnection.disconnect() - } - - override fun getPrivate() = privateKey - - override fun getPublic() = publicKey - - override fun getType(): KeyType = KeyType.fromKey(publicKey) -} diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/OpenKeychainWrappedKeyAlgorithmFactory.kt b/app/src/main/java/app/passwordstore/util/git/sshj/OpenKeychainWrappedKeyAlgorithmFactory.kt deleted file mode 100644 index 611ba9e0..00000000 --- a/app/src/main/java/app/passwordstore/util/git/sshj/OpenKeychainWrappedKeyAlgorithmFactory.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package app.passwordstore.util.git.sshj - -import com.hierynomus.sshj.key.KeyAlgorithm -import java.io.ByteArrayOutputStream -import java.security.PrivateKey -import java.security.interfaces.ECKey -import kotlinx.coroutines.runBlocking -import net.schmizz.sshj.common.Buffer -import net.schmizz.sshj.common.Factory -import net.schmizz.sshj.signature.Signature -import org.openintents.ssh.authentication.SshAuthenticationApi - -interface OpenKeychainPrivateKey : PrivateKey, ECKey { - - suspend fun sign(challenge: ByteArray, hashAlgorithm: Int): ByteArray - - override fun getFormat() = null - override fun getEncoded() = null -} - -class OpenKeychainWrappedKeyAlgorithmFactory(private val factory: Factory.Named<KeyAlgorithm>) : - Factory.Named<KeyAlgorithm> by factory { - - override fun create() = OpenKeychainWrappedKeyAlgorithm(factory.create()) -} - -class OpenKeychainWrappedKeyAlgorithm(private val keyAlgorithm: KeyAlgorithm) : - KeyAlgorithm by keyAlgorithm { - - private val hashAlgorithm = - when (keyAlgorithm.keyAlgorithm) { - "rsa-sha2-512" -> SshAuthenticationApi.SHA512 - "rsa-sha2-256" -> SshAuthenticationApi.SHA256 - "ssh-rsa", - "ssh-rsa-cert-v01@openssh.com" -> SshAuthenticationApi.SHA1 - // Other algorithms don't use this value, but it has to be valid. - else -> SshAuthenticationApi.SHA512 - } - - override fun newSignature() = - OpenKeychainWrappedSignature(keyAlgorithm.newSignature(), hashAlgorithm) -} - -class OpenKeychainWrappedSignature( - private val wrappedSignature: Signature, - private val hashAlgorithm: Int -) : Signature by wrappedSignature { - - private val data = ByteArrayOutputStream() - - private var bridgedPrivateKey: OpenKeychainPrivateKey? = null - - override fun initSign(prvkey: PrivateKey?) { - if (prvkey is OpenKeychainPrivateKey) { - bridgedPrivateKey = prvkey - } else { - wrappedSignature.initSign(prvkey) - } - } - - override fun update(H: ByteArray?) { - if (bridgedPrivateKey != null) { - data.write(H!!) - } else { - wrappedSignature.update(H) - } - } - - override fun update(H: ByteArray?, off: Int, len: Int) { - if (bridgedPrivateKey != null) { - data.write(H!!, off, len) - } else { - wrappedSignature.update(H, off, len) - } - } - - override fun sign(): ByteArray? = - if (bridgedPrivateKey != null) { - runBlocking { bridgedPrivateKey!!.sign(data.toByteArray(), hashAlgorithm) } - } else { - wrappedSignature.sign() - } - - override fun encode(signature: ByteArray?): ByteArray? = - if (bridgedPrivateKey != null) { - require(signature != null) { "OpenKeychain signature must not be null" } - val encodedSignature = Buffer.PlainBuffer(signature) - // We need to drop the algorithm name and extract the raw signature since SSHJ adds the - // name - // later. - encodedSignature.readString() - encodedSignature.readBytes().also { - bridgedPrivateKey = null - data.reset() - } - } else { - wrappedSignature.encode(signature) - } -} diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt index 7522de50..5601a253 100644 --- a/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt +++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt @@ -232,16 +232,15 @@ class SshjConfig : ConfigImpl() { private fun initKeyAlgorithms() { keyAlgorithms = listOf( - KeyAlgorithms.SSHRSACertV01(), - KeyAlgorithms.EdDSA25519(), - KeyAlgorithms.ECDSASHANistp521(), - KeyAlgorithms.ECDSASHANistp384(), - KeyAlgorithms.ECDSASHANistp256(), - KeyAlgorithms.RSASHA512(), - KeyAlgorithms.RSASHA256(), - KeyAlgorithms.SSHRSA(), - ) - .map { OpenKeychainWrappedKeyAlgorithmFactory(it) } + KeyAlgorithms.SSHRSACertV01(), + KeyAlgorithms.EdDSA25519(), + KeyAlgorithms.ECDSASHANistp521(), + KeyAlgorithms.ECDSASHANistp384(), + KeyAlgorithms.ECDSASHANistp256(), + KeyAlgorithms.RSASHA512(), + KeyAlgorithms.RSASHA256(), + KeyAlgorithms.SSHRSA(), + ) } private fun initRandomFactory() { diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt index b7f0542f..58af8495 100644 --- a/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt +++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt @@ -5,6 +5,7 @@ package app.passwordstore.util.git.sshj import android.util.Base64 +import androidx.appcompat.app.AppCompatActivity import app.passwordstore.util.git.operation.CredentialFinder import app.passwordstore.util.settings.AuthMode import com.github.michaelbull.result.getOrElse @@ -41,10 +42,9 @@ import org.eclipse.jgit.transport.SshSessionFactory import org.eclipse.jgit.transport.URIish import org.eclipse.jgit.util.FS -sealed class SshAuthMethod(val activity: ContinuationContainerActivity) { - class Password(activity: ContinuationContainerActivity) : SshAuthMethod(activity) - class SshKey(activity: ContinuationContainerActivity) : SshAuthMethod(activity) - class OpenKeychain(activity: ContinuationContainerActivity) : SshAuthMethod(activity) +sealed class SshAuthMethod(val activity: AppCompatActivity) { + class Password(activity: AppCompatActivity) : SshAuthMethod(activity) + class SshKey(activity: AppCompatActivity) : SshAuthMethod(activity) } abstract class InteractivePasswordFinder : PasswordFinder { @@ -157,14 +157,6 @@ private class SshjSession( AuthPublickey(SshKey.provide(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey))) ssh.auth(username, pubkeyAuth, passwordAuth) } - is SshAuthMethod.OpenKeychain -> { - runBlocking { - OpenKeychainKeyProvider.prepareAndUse(authMethod.activity) { provider -> - val openKeychainAuth = AuthPublickey(provider) - ssh.auth(username, openKeychainAuth, passwordAuth) - } - } - } } return this } diff --git a/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt b/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt index 1baf9640..ed1c02dc 100644 --- a/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt +++ b/app/src/main/java/app/passwordstore/util/settings/GitSettings.kt @@ -37,7 +37,6 @@ enum class Protocol(val pref: String) { enum class AuthMode(val pref: String) { SshKey("ssh-key"), Password("username/password"), - OpenKeychain("OpenKeychain"), None("None"), ; @@ -156,7 +155,7 @@ constructor( ) return UpdateConnectionSettingsResult.MissingUsername(newProtocol) val validHttpsAuth = listOf(AuthMode.None, AuthMode.Password) - val validSshAuth = listOf(AuthMode.OpenKeychain, AuthMode.Password, AuthMode.SshKey) + val validSshAuth = listOf(AuthMode.Password, AuthMode.SshKey) when { newProtocol == Protocol.Https && newAuthMode !in validHttpsAuth -> { return UpdateConnectionSettingsResult.AuthModeMismatch(newProtocol, validHttpsAuth) |