diff options
| author | Harsh Shandilya <me@msfjarvis.dev> | 2022-07-15 00:53:48 +0530 |
|---|---|---|
| committer | Harsh Shandilya <me@msfjarvis.dev> | 2022-07-15 01:13:47 +0530 |
| commit | 549ee790d3e52bc62565ddf92e6a556e98b5195e (patch) | |
| tree | d5758e5eb80093704e683c8da926838e18182588 /app/src/main/java/dev/msfjarvis/aps/util/git | |
| parent | 010c6e227c9cc27f4d01bc912311f977b2aeb3a7 (diff) | |
| download | HardenedPass-549ee790d3e52bc62565ddf92e6a556e98b5195e.tar.gz HardenedPass-549ee790d3e52bc62565ddf92e6a556e98b5195e.zip | |
all: re-do package structure yet again
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/util/git')
19 files changed, 0 insertions, 2102 deletions
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/ErrorMessages.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/ErrorMessages.kt deleted file mode 100644 index 36dc445f..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/ErrorMessages.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package dev.msfjarvis.aps.util.git - -import android.os.RemoteException -import androidx.annotation.StringRes -import dev.msfjarvis.aps.Application -import dev.msfjarvis.aps.R -import java.net.UnknownHostException - -/** - * Supertype for all Git-related [Exception] s that can be thrown by [GitCommandExecutor.execute]. - */ -sealed class GitException(@StringRes res: Int, vararg fmt: String) : - Exception(buildMessage(res, *fmt)) { - - override val message = super.message!! - - companion object { - - private fun buildMessage(@StringRes res: Int, vararg fmt: String) = - Application.instance.resources.getString(res, *fmt) - } - - /** Encapsulates possible errors from a [org.eclipse.jgit.api.PullCommand]. */ - sealed class PullException(@StringRes res: Int, vararg fmt: String) : GitException(res, *fmt) { - - object PullRebaseFailed : PullException(R.string.git_pull_rebase_fail_error) - object PullMergeFailed : PullException(R.string.git_pull_merge_fail_error) - } - - /** Encapsulates possible errors from a [org.eclipse.jgit.api.PushCommand]. */ - sealed class PushException(@StringRes res: Int, vararg fmt: String) : GitException(res, *fmt) { - - object NonFastForward : PushException(R.string.git_push_nff_error) - object RemoteRejected : PushException(R.string.git_push_other_error) - class Generic(message: String) : PushException(R.string.git_push_generic_error, message) - } -} - -object ErrorMessages { - - operator fun get(throwable: Throwable?): String { - val resources = Application.instance.resources - if (throwable == null) return resources.getString(R.string.git_unknown_error) - return when (val rootCause = rootCause(throwable)) { - is GitException -> rootCause.message - is UnknownHostException -> resources.getString(R.string.git_unknown_host, throwable.message) - else -> throwable.message ?: resources.getString(R.string.git_unknown_error) - } - } - - private fun rootCause(throwable: Throwable): Throwable { - var cause = throwable - while (cause.cause != null) { - if (cause is GitException) break - val nextCause = cause.cause!! - if (nextCause is RemoteException) break - cause = nextCause - } - return cause - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/GitCommandExecutor.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/GitCommandExecutor.kt deleted file mode 100644 index e6eb77f5..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/GitCommandExecutor.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package dev.msfjarvis.aps.util.git - -import android.widget.Toast -import androidx.fragment.app.FragmentActivity -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.runCatching -import com.google.android.material.snackbar.Snackbar -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent -import dev.msfjarvis.aps.R -import dev.msfjarvis.aps.util.extensions.snackbar -import dev.msfjarvis.aps.util.extensions.unsafeLazy -import dev.msfjarvis.aps.util.git.GitException.PullException -import dev.msfjarvis.aps.util.git.GitException.PushException -import dev.msfjarvis.aps.util.git.operation.GitOperation -import dev.msfjarvis.aps.util.settings.GitSettings -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.eclipse.jgit.api.CommitCommand -import org.eclipse.jgit.api.PullCommand -import org.eclipse.jgit.api.PushCommand -import org.eclipse.jgit.api.StatusCommand -import org.eclipse.jgit.lib.PersonIdent -import org.eclipse.jgit.transport.RemoteRefUpdate - -class GitCommandExecutor( - private val activity: FragmentActivity, - private val operation: GitOperation, -) { - - private val hiltEntryPoint by unsafeLazy { - EntryPointAccessors.fromApplication( - activity.applicationContext, - GitCommandExecutorEntryPoint::class.java - ) - } - suspend fun execute(): Result<Unit, Throwable> { - val gitSettings = hiltEntryPoint.gitSettings() - val snackbar = - activity.snackbar( - message = activity.resources.getString(R.string.git_operation_running), - length = Snackbar.LENGTH_INDEFINITE, - ) - // Count the number of uncommitted files - var nbChanges = 0 - return runCatching { - for (command in operation.commands) { - when (command) { - is StatusCommand -> { - val res = withContext(Dispatchers.IO) { command.call() } - nbChanges = res.uncommittedChanges.size - } - is CommitCommand -> { - // the previous status will eventually be used to avoid a commit - if (nbChanges > 0) { - withContext(Dispatchers.IO) { - val name = gitSettings.authorName.ifEmpty { "root" } - val email = gitSettings.authorEmail.ifEmpty { "localhost" } - val identity = PersonIdent(name, email) - command.setAuthor(identity).setCommitter(identity).call() - } - } - } - is PullCommand -> { - val result = withContext(Dispatchers.IO) { command.call() } - if (result.rebaseResult != null) { - if (!result.rebaseResult.status.isSuccessful) { - throw PullException.PullRebaseFailed - } - } else if (result.mergeResult != null) { - if (!result.mergeResult.mergeStatus.isSuccessful) { - throw PullException.PullMergeFailed - } - } - } - is PushCommand -> { - val results = withContext(Dispatchers.IO) { command.call() } - for (result in results) { - // Code imported (modified) from Gerrit PushOp, license Apache v2 - for (rru in result.remoteUpdates) { - when (rru.status) { - RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> - throw PushException.NonFastForward - RemoteRefUpdate.Status.REJECTED_NODELETE, - RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED, - RemoteRefUpdate.Status.NON_EXISTING, - RemoteRefUpdate.Status.NOT_ATTEMPTED, -> - throw PushException.Generic(rru.status.name) - RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> { - throw if ("non-fast-forward" == rru.message) { - PushException.RemoteRejected - } else { - PushException.Generic(rru.message) - } - } - RemoteRefUpdate.Status.UP_TO_DATE -> { - withContext(Dispatchers.Main) { - Toast.makeText( - activity.applicationContext, - activity.applicationContext.getString(R.string.git_push_up_to_date), - Toast.LENGTH_SHORT - ) - .show() - } - } - else -> {} - } - } - } - } - else -> { - withContext(Dispatchers.IO) { command.call() } - } - } - } - } - .also { snackbar.dismiss() } - } - - @EntryPoint - @InstallIn(SingletonComponent::class) - interface GitCommandExecutorEntryPoint { - - fun gitSettings(): GitSettings - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/GitCommit.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/GitCommit.kt deleted file mode 100644 index 490f5e2c..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/GitCommit.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package dev.msfjarvis.aps.util.git - -import java.util.Date - -/** - * Basic information about a git commit. - * - * @property hash full-length hash of the commit object. - * @property shortMessage the commit's short message (i.e. title line). - * @property authorName name of the commit's author without email address. - * @property time time when the commit was created. - */ -data class GitCommit( - val hash: String, - val shortMessage: String, - val authorName: String, - val time: Date -) diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/GitLogModel.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/GitLogModel.kt deleted file mode 100644 index ed210a65..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/GitLogModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package dev.msfjarvis.aps.util.git - -import com.github.michaelbull.result.getOrElse -import com.github.michaelbull.result.runCatching -import dev.msfjarvis.aps.data.repo.PasswordRepository -import dev.msfjarvis.aps.util.extensions.asLog -import dev.msfjarvis.aps.util.extensions.hash -import dev.msfjarvis.aps.util.extensions.time -import dev.msfjarvis.aps.util.extensions.unsafeLazy -import logcat.LogPriority.ERROR -import logcat.logcat -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.revwalk.RevCommit - -private val TAG = GitLogModel::class.java.simpleName - -private fun commits(): Iterable<RevCommit> { - val repo = PasswordRepository.repository - if (repo == null) { - logcat(TAG, ERROR) { "Could not access git repository" } - return listOf() - } - return runCatching { Git(repo).log().call() } - .getOrElse { e -> - logcat(TAG, ERROR) { e.asLog("Failed to obtain git commits") } - listOf() - } -} - -/** - * Provides [GitCommit] s from a git-log of the password git repository. - * - * All commits are acquired on the first request to this object. - */ -class GitLogModel { - - // All commits are acquired here at once. Acquiring the commits in batches would not have been - // entirely sensible because the amount of computation required to obtain commit number n from - // the log includes the amount of computation required to obtain commit number n-1 from the log. - // This is because the commit graph is walked from HEAD to the last commit to obtain. - // Additionally, tests with 1000 commits in the log have not produced a significant delay in the - // user experience. - private val cache: MutableList<GitCommit> by unsafeLazy { - commits() - .map { GitCommit(it.hash, it.shortMessage, it.authorIdent.name, it.time) } - .toMutableList() - } - val size = cache.size - - fun get(index: Int): GitCommit? { - if (index >= size) - logcat(ERROR) { "Cannot get git commit with index $index. There are only $size." } - return cache.getOrNull(index) - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/BreakOutOfDetached.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/BreakOutOfDetached.kt deleted file mode 100644 index 6ea9b8bb..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/BreakOutOfDetached.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.git.operation - -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dev.msfjarvis.aps.R -import dev.msfjarvis.aps.util.extensions.unsafeLazy -import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity -import org.eclipse.jgit.api.RebaseCommand -import org.eclipse.jgit.api.ResetCommand -import org.eclipse.jgit.lib.RepositoryState - -class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) : - GitOperation(callingActivity) { - - private val merging = repository.repositoryState == RepositoryState.MERGING - private val resetCommands = - arrayOf( - // git checkout -b conflict-branch - git - .checkout() - .setCreateBranch(true) - .setName("conflicting-$remoteBranch-${System.currentTimeMillis()}"), - // push the changes - git.push().setRemote("origin"), - // switch back to ${gitBranch} - git.checkout().setName(remoteBranch), - ) - - override val commands by unsafeLazy { - if (merging) { - // We need to run some non-command operations first - repository.writeMergeCommitMsg(null) - repository.writeMergeHeads(null) - arrayOf( - // reset hard back to our local HEAD - git.reset().setMode(ResetCommand.ResetType.HARD), - *resetCommands, - ) - } else { - arrayOf( - // abort the rebase - git.rebase().setOperation(RebaseCommand.Operation.ABORT), - *resetCommands, - ) - } - } - - override fun preExecute() = - if (!git.repository.repositoryState.isRebasing && !merging) { - MaterialAlertDialogBuilder(callingActivity) - .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title)) - .setMessage( - callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded) - ) - .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> - callingActivity.finish() - } - .show() - false - } else { - true - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/CloneOperation.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/CloneOperation.kt deleted file mode 100644 index 75db2ea5..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/CloneOperation.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.git.operation - -import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.api.GitCommand - -/** - * Creates a new clone operation - * - * @param uri URL to clone the repository from - * @param callingActivity the calling activity - */ -class CloneOperation(callingActivity: ContinuationContainerActivity, uri: String) : - GitOperation(callingActivity) { - - override val commands: Array<GitCommand<out Any>> = - arrayOf( - Git.cloneRepository().setBranch(remoteBranch).setDirectory(repository.workTree).setURI(uri), - ) -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/CredentialFinder.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/CredentialFinder.kt deleted file mode 100644 index 623d69ea..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/CredentialFinder.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package dev.msfjarvis.aps.util.git.operation - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import android.view.LayoutInflater -import android.view.WindowManager -import androidx.annotation.StringRes -import androidx.core.content.edit -import androidx.core.widget.doOnTextChanged -import androidx.fragment.app.FragmentActivity -import com.google.android.material.checkbox.MaterialCheckBox -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent -import dev.msfjarvis.aps.R -import dev.msfjarvis.aps.injection.prefs.GitPreferences -import dev.msfjarvis.aps.util.git.sshj.InteractivePasswordFinder -import dev.msfjarvis.aps.util.settings.AuthMode -import dev.msfjarvis.aps.util.settings.PreferenceKeys -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume - -class CredentialFinder(val callingActivity: FragmentActivity, val authMode: AuthMode) : - InteractivePasswordFinder() { - - private val hiltEntryPoint = - EntryPointAccessors.fromApplication( - callingActivity.applicationContext, - CredentialFinderEntryPoint::class.java, - ) - - override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) { - val gitOperationPrefs = hiltEntryPoint.gitPrefs() - val credentialPref: String - @StringRes val messageRes: Int - @StringRes val hintRes: Int - @StringRes val rememberRes: Int - @StringRes val errorRes: Int - when (authMode) { - AuthMode.SshKey -> { - credentialPref = PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE - messageRes = R.string.passphrase_dialog_text - hintRes = R.string.ssh_keygen_passphrase - rememberRes = R.string.git_operation_remember_passphrase - errorRes = R.string.git_operation_wrong_passphrase - } - AuthMode.Password -> { - // Could be either an SSH or an HTTPS password - credentialPref = PreferenceKeys.HTTPS_PASSWORD - messageRes = R.string.password_dialog_text - hintRes = R.string.git_operation_hint_password - rememberRes = R.string.git_operation_remember_password - errorRes = R.string.git_operation_wrong_password - } - else -> - throw IllegalStateException("Only SshKey and Password connection mode ask for passwords") - } - if (isRetry) gitOperationPrefs.edit { remove(credentialPref) } - val storedCredential = gitOperationPrefs.getString(credentialPref, null) - if (storedCredential == null) { - val layoutInflater = LayoutInflater.from(callingActivity) - - @SuppressLint("InflateParams") - val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null) - val credentialLayout = - dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout) - val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential) - editCredential.setHint(hintRes) - val rememberCredential = - dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential) - rememberCredential.setText(rememberRes) - if (isRetry) { - credentialLayout.error = callingActivity.resources.getString(errorRes) - // Reset error when user starts entering a password - editCredential.doOnTextChanged { _, _, _, _ -> credentialLayout.error = null } - } - MaterialAlertDialogBuilder(callingActivity) - .run { - setTitle(R.string.passphrase_dialog_title) - setMessage(messageRes) - setView(dialogView) - setPositiveButton(R.string.dialog_ok) { _, _ -> - val credential = editCredential.text.toString() - if (rememberCredential.isChecked) { - gitOperationPrefs.edit { putString(credentialPref, credential) } - } - cont.resume(credential) - } - setNegativeButton(R.string.dialog_cancel) { _, _ -> cont.resume(null) } - setOnCancelListener { cont.resume(null) } - create() - } - .run { - window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) - show() - } - } else { - cont.resume(storedCredential) - } - } - - @EntryPoint - @InstallIn(SingletonComponent::class) - interface CredentialFinderEntryPoint { - @GitPreferences fun gitPrefs(): SharedPreferences - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/GcOperation.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/GcOperation.kt deleted file mode 100644 index c070027e..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/GcOperation.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package dev.msfjarvis.aps.util.git.operation - -import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity -import org.eclipse.jgit.api.GitCommand - -/** - * Run an aggressive garbage collection job on the repository, expiring every loose object to - * achieve the best compression. - */ -class GcOperation( - callingActivity: ContinuationContainerActivity, -) : GitOperation(callingActivity) { - - override val requiresAuth: Boolean = false - override val commands: Array<GitCommand<out Any>> = - arrayOf(git.gc().setAggressive(true).setExpire(null)) -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/GitOperation.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/GitOperation.kt deleted file mode 100644 index 576a802d..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/GitOperation.kt +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.git.operation - -import android.content.Intent -import android.widget.Toast -import androidx.fragment.app.FragmentActivity -import com.github.michaelbull.result.Err -import com.github.michaelbull.result.Ok -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent -import dev.msfjarvis.aps.R -import dev.msfjarvis.aps.data.repo.PasswordRepository -import dev.msfjarvis.aps.ui.sshkeygen.SshKeyGenActivity -import dev.msfjarvis.aps.ui.sshkeygen.SshKeyImportActivity -import dev.msfjarvis.aps.util.auth.BiometricAuthenticator -import dev.msfjarvis.aps.util.auth.BiometricAuthenticator.Result.* -import dev.msfjarvis.aps.util.git.GitCommandExecutor -import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity -import dev.msfjarvis.aps.util.git.sshj.SshAuthMethod -import dev.msfjarvis.aps.util.git.sshj.SshKey -import dev.msfjarvis.aps.util.git.sshj.SshjSessionFactory -import dev.msfjarvis.aps.util.settings.AuthMode -import dev.msfjarvis.aps.util.settings.GitSettings -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat -import net.schmizz.sshj.common.DisconnectReason -import net.schmizz.sshj.common.SSHException -import net.schmizz.sshj.userauth.password.PasswordFinder -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.api.GitCommand -import org.eclipse.jgit.api.TransportCommand -import org.eclipse.jgit.errors.UnsupportedCredentialItem -import org.eclipse.jgit.transport.CredentialItem -import org.eclipse.jgit.transport.CredentialsProvider -import org.eclipse.jgit.transport.SshTransport -import org.eclipse.jgit.transport.Transport -import org.eclipse.jgit.transport.URIish - -/** - * Creates a new git operation - * - * @param callingActivity the calling activity - */ -abstract class GitOperation(protected val callingActivity: FragmentActivity) { - - /** List of [GitCommand]s that are executed by an operation. */ - abstract val commands: Array<GitCommand<out Any>> - - /** Whether the operation requires authentication or not. */ - open val requiresAuth: Boolean = true - private val hostKeyFile = callingActivity.filesDir.resolve(".host_key") - private var sshSessionFactory: SshjSessionFactory? = null - private val hiltEntryPoint = - EntryPointAccessors.fromApplication( - callingActivity.applicationContext, - GitOperationEntryPoint::class.java - ) - - protected val repository = PasswordRepository.repository!! - protected val git = Git(repository) - protected val remoteBranch = hiltEntryPoint.gitSettings().branch - private val authActivity - get() = callingActivity as ContinuationContainerActivity - - private class HttpsCredentialsProvider(private val passwordFinder: PasswordFinder) : - CredentialsProvider() { - - private var cachedPassword: CharArray? = null - - override fun isInteractive() = true - - override fun get(uri: URIish?, vararg items: CredentialItem): Boolean { - for (item in items) { - when (item) { - is CredentialItem.Username -> item.value = uri?.user - is CredentialItem.Password -> { - item.value = - cachedPassword?.clone() - ?: passwordFinder.reqPassword(null).also { cachedPassword = it.clone() } - } - else -> UnsupportedCredentialItem(uri, item.javaClass.name) - } - } - return true - } - - override fun supports(vararg items: CredentialItem) = - items.all { it is CredentialItem.Username || it is CredentialItem.Password } - - override fun reset(uri: URIish?) { - cachedPassword?.fill(0.toChar()) - cachedPassword = null - } - } - - private fun getSshKey(make: Boolean) { - runCatching { - val intent = - if (make) { - Intent(callingActivity.applicationContext, SshKeyGenActivity::class.java) - } else { - Intent(callingActivity.applicationContext, SshKeyImportActivity::class.java) - } - callingActivity.startActivity(intent) - } - .onFailure { e -> logcat(ERROR) { e.asLog() } } - } - - private fun registerAuthProviders( - authMethod: SshAuthMethod, - credentialsProvider: CredentialsProvider? = null - ) { - sshSessionFactory = SshjSessionFactory(authMethod, hostKeyFile) - commands.filterIsInstance<TransportCommand<*, *>>().forEach { command -> - command.setTransportConfigCallback { transport: Transport -> - (transport as? SshTransport)?.sshSessionFactory = sshSessionFactory - credentialsProvider?.let { transport.credentialsProvider = it } - } - command.setTimeout(CONNECT_TIMEOUT) - } - } - - /** Executes the GitCommand in an async task. */ - suspend fun execute(): Result<Unit, Throwable> { - if (!preExecute()) { - return Ok(Unit) - } - val operationResult = - GitCommandExecutor( - callingActivity, - this, - ) - .execute() - postExecute() - return operationResult - } - - private fun onMissingSshKeyFile() { - MaterialAlertDialogBuilder(callingActivity) - .setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text)) - .setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title)) - .setPositiveButton( - callingActivity.resources.getString(R.string.ssh_preferences_dialog_import) - ) { _, _ -> getSshKey(false) } - .setNegativeButton( - callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate) - ) { _, _ -> getSshKey(true) } - .setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> - // Finish the blank GitActivity so user doesn't have to press back - callingActivity.finish() - } - .show() - } - - suspend fun executeAfterAuthentication(authMode: AuthMode): Result<Unit, Throwable> { - when (authMode) { - AuthMode.SshKey -> - if (SshKey.exists) { - if (SshKey.mustAuthenticate) { - val result = - withContext(Dispatchers.Main) { - suspendCoroutine<BiometricAuthenticator.Result> { cont -> - BiometricAuthenticator.authenticate( - callingActivity, - R.string.biometric_prompt_title_ssh_auth - ) { result -> if (result !is Failure) cont.resume(result) } - } - } - when (result) { - is Success -> { - registerAuthProviders(SshAuthMethod.SshKey(authActivity)) - } - is Cancelled -> { - return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER)) - } - is Failure -> { - throw IllegalStateException("Biometric authentication failures should be ignored") - } - else -> { - // There is a chance we succeed if the user recently confirmed - // their screen lock. Doing so would have a potential to confuse - // users though, who might deduce that the screen lock - // protection is not effective. Hence, we fail with an error. - Toast.makeText( - callingActivity.applicationContext, - R.string.biometric_auth_generic_failure, - Toast.LENGTH_LONG - ) - .show() - callingActivity.finish() - } - } - } else { - registerAuthProviders(SshAuthMethod.SshKey(authActivity)) - } - } else { - onMissingSshKeyFile() - // This would correctly cancel the operation but won't surface a user-visible - // 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)) - registerAuthProviders(SshAuthMethod.Password(authActivity), httpsCredentialProvider) - } - AuthMode.None -> {} - } - return execute() - } - - /** Called before execution of the Git operation. Return false to cancel. */ - open fun preExecute() = true - - private suspend fun postExecute() { - withContext(Dispatchers.IO) { sshSessionFactory?.close() } - } - - companion object { - - /** Timeout in seconds before [TransportCommand] will abort a stalled IO operation. */ - private const val CONNECT_TIMEOUT = 10 - } - - @EntryPoint - @InstallIn(SingletonComponent::class) - interface GitOperationEntryPoint { - fun gitSettings(): GitSettings - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/PullOperation.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/PullOperation.kt deleted file mode 100644 index 394b7cb4..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/PullOperation.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.git.operation - -import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity -import org.eclipse.jgit.api.GitCommand - -class PullOperation( - callingActivity: ContinuationContainerActivity, - rebase: Boolean, -) : GitOperation(callingActivity) { - - /** - * The story of why the pull operation is committing files goes like this: Once upon a time when - * the world was burning and Blade Runner 2049 was real life (in the worst way), we were made - * aware that Bitbucket is actually bad, and disables a neat OpenSSH feature called multiplexing. - * So now, rather than being able to do a [SyncOperation], we'd have to first do a [PullOperation] - * and then a [PushOperation]. To make the behavior identical despite this suboptimal situation, - * we opted to replicate [SyncOperation]'s committing flow within [PullOperation], almost exactly - * replicating [SyncOperation] but leaving the pushing part to [PushOperation]. - */ - override val commands: Array<GitCommand<out Any>> = - arrayOf( - // Stage all files - git.add().addFilepattern("."), - // Populate the changed files count - git.status(), - // Commit everything! If needed, obviously. - git.commit().setAll(true).setMessage("[Android Password Store] Sync"), - // Pull and rebase on top of the remote branch - git.pull().setRebase(rebase).setRemote("origin"), - ) -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/PushOperation.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/PushOperation.kt deleted file mode 100644 index 14f16164..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/PushOperation.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.git.operation - -import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity -import org.eclipse.jgit.api.GitCommand - -class PushOperation(callingActivity: ContinuationContainerActivity) : - GitOperation(callingActivity) { - - override val commands: Array<GitCommand<out Any>> = - arrayOf( - git.push().setPushAll().setRemote("origin"), - ) -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/ResetToRemoteOperation.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/ResetToRemoteOperation.kt deleted file mode 100644 index 16114f65..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/ResetToRemoteOperation.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.git.operation - -import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity -import org.eclipse.jgit.api.ResetCommand - -class ResetToRemoteOperation(callingActivity: ContinuationContainerActivity) : - GitOperation(callingActivity) { - - override val commands = - arrayOf( - // Stage all files - git.add().addFilepattern("."), - // Fetch everything from the origin remote - git.fetch().setRemote("origin"), - // Do a hard reset to the remote branch. Equivalent to git reset --hard - // origin/$remoteBranch - git.reset().setRef("origin/$remoteBranch").setMode(ResetCommand.ResetType.HARD), - // Force-create $remoteBranch if it doesn't exist. This covers the case where you - // switched - // branches from 'master' to anything else. - git.branchCreate().setName(remoteBranch).setForce(true), - ) -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/SyncOperation.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/SyncOperation.kt deleted file mode 100644 index 589c6305..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/SyncOperation.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.git.operation - -import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity - -class SyncOperation( - callingActivity: ContinuationContainerActivity, - rebase: Boolean, -) : GitOperation(callingActivity) { - - override val commands = - arrayOf( - // Stage all files - git.add().addFilepattern("."), - // Populate the changed files count - git.status(), - // Commit everything! If needed, obviously. - git.commit().setAll(true).setMessage("[Android Password Store] Sync"), - // Pull and rebase on top of the remote branch - git.pull().setRebase(rebase).setRemote("origin"), - // Push it all back - git.push().setPushAll().setRemote("origin"), - ) -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/ContinuationContainerActivity.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/ContinuationContainerActivity.kt deleted file mode 100644 index 523ff5b6..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/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 dev.msfjarvis.aps.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/dev/msfjarvis/aps/util/git/sshj/OpenKeychainKeyProvider.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/OpenKeychainKeyProvider.kt deleted file mode 100644 index 8436d1ce..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/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 dev.msfjarvis.aps.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 dev.msfjarvis.aps.util.extensions.OPENPGP_PROVIDER -import dev.msfjarvis.aps.util.extensions.sharedPrefs -import dev.msfjarvis.aps.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/dev/msfjarvis/aps/util/git/sshj/OpenKeychainWrappedKeyAlgorithmFactory.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/OpenKeychainWrappedKeyAlgorithmFactory.kt deleted file mode 100644 index c5cb6eaa..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/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 dev.msfjarvis.aps.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/dev/msfjarvis/aps/util/git/sshj/SshKey.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshKey.kt deleted file mode 100644 index ea96af53..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshKey.kt +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.git.sshj - -import android.content.Context -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.provider.OpenableColumns -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyInfo -import android.security.keystore.KeyProperties -import android.util.Base64 -import androidx.core.content.edit -import androidx.security.crypto.EncryptedFile -import androidx.security.crypto.MasterKey -import com.github.michaelbull.result.getOrElse -import com.github.michaelbull.result.runCatching -import dev.msfjarvis.aps.Application -import dev.msfjarvis.aps.R -import dev.msfjarvis.aps.util.extensions.getEncryptedGitPrefs -import dev.msfjarvis.aps.util.extensions.getString -import dev.msfjarvis.aps.util.extensions.sharedPrefs -import dev.msfjarvis.aps.util.extensions.unsafeLazy -import dev.msfjarvis.aps.util.settings.PreferenceKeys -import java.io.File -import java.io.IOException -import java.security.KeyFactory -import java.security.KeyPairGenerator -import java.security.KeyStore -import java.security.PrivateKey -import java.security.PublicKey -import javax.crypto.SecretKey -import javax.crypto.SecretKeyFactory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import logcat.asLog -import logcat.logcat -import net.i2p.crypto.eddsa.EdDSAPrivateKey -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable -import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec -import net.schmizz.sshj.SSHClient -import net.schmizz.sshj.common.Buffer -import net.schmizz.sshj.common.KeyType -import net.schmizz.sshj.userauth.keyprovider.KeyProvider - -private const val PROVIDER_ANDROID_KEY_STORE = "AndroidKeyStore" -private const val KEYSTORE_ALIAS = "sshkey" -private const val ANDROIDX_SECURITY_KEYSET_PREF_NAME = "androidx_sshkey_keyset_prefs" - -private val androidKeystore: KeyStore by unsafeLazy { - KeyStore.getInstance(PROVIDER_ANDROID_KEY_STORE).apply { load(null) } -} - -private val KeyStore.sshPrivateKey - get() = getKey(KEYSTORE_ALIAS, null) as? PrivateKey - -private val KeyStore.sshPublicKey - get() = getCertificate(KEYSTORE_ALIAS)?.publicKey - -fun parseSshPublicKey(sshPublicKey: String): PublicKey? { - val sshKeyParts = sshPublicKey.split("""\s+""".toRegex()) - if (sshKeyParts.size < 2) return null - return Buffer.PlainBuffer(Base64.decode(sshKeyParts[1], Base64.NO_WRAP)).readPublicKey() -} - -fun toSshPublicKey(publicKey: PublicKey): String { - val rawPublicKey = Buffer.PlainBuffer().putPublicKey(publicKey).compactData - val keyType = KeyType.fromKey(publicKey) - return "$keyType ${Base64.encodeToString(rawPublicKey, Base64.NO_WRAP)}" -} - -object SshKey { - - val sshPublicKey - get() = if (publicKeyFile.exists()) publicKeyFile.readText() else null - val canShowSshPublicKey - get() = type in listOf(Type.LegacyGenerated, Type.KeystoreNative, Type.KeystoreWrappedEd25519) - val exists - get() = type != null - val mustAuthenticate: Boolean - get() { - return runCatching { - if (type !in listOf(Type.KeystoreNative, Type.KeystoreWrappedEd25519)) return false - when (val key = androidKeystore.getKey(KEYSTORE_ALIAS, null)) { - is PrivateKey -> { - val factory = KeyFactory.getInstance(key.algorithm, PROVIDER_ANDROID_KEY_STORE) - return factory.getKeySpec(key, KeyInfo::class.java).isUserAuthenticationRequired - } - is SecretKey -> { - val factory = SecretKeyFactory.getInstance(key.algorithm, PROVIDER_ANDROID_KEY_STORE) - (factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo).isUserAuthenticationRequired - } - else -> throw IllegalStateException("SSH key does not exist in Keystore") - } - } - .getOrElse { error -> - // It is fine to swallow the exception here since it will reappear when the key - // is - // used for SSH authentication and can then be shown in the UI. - logcat { error.asLog() } - false - } - } - - private val context: Context - get() = Application.instance.applicationContext - - private val privateKeyFile - get() = File(context.filesDir, ".ssh_key") - private val publicKeyFile - get() = File(context.filesDir, ".ssh_key.pub") - - private var type: Type? - get() = Type.fromValue(context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_KEY_TYPE)) - set(value) = - context.sharedPrefs.edit { putString(PreferenceKeys.GIT_REMOTE_KEY_TYPE, value?.value) } - - private val isStrongBoxSupported by unsafeLazy { - if (Build.VERSION.SDK_INT >= 28) - context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) - else false - } - - private enum class Type(val value: String) { - Imported("imported"), - KeystoreNative("keystore_native"), - KeystoreWrappedEd25519("keystore_wrapped_ed25519"), - - // Behaves like `Imported`, but allows to view the public key. - LegacyGenerated("legacy_generated"), - ; - - companion object { - - fun fromValue(value: String?): Type? = values().associateBy { it.value }[value] - } - } - - enum class Algorithm( - val algorithm: String, - val applyToSpec: KeyGenParameterSpec.Builder.() -> Unit - ) { - Rsa( - KeyProperties.KEY_ALGORITHM_RSA, - { - setKeySize(3072) - setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) - setDigests( - KeyProperties.DIGEST_SHA1, - KeyProperties.DIGEST_SHA256, - KeyProperties.DIGEST_SHA512 - ) - } - ), - Ecdsa( - KeyProperties.KEY_ALGORITHM_EC, - { - setKeySize(256) - setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1")) - setDigests(KeyProperties.DIGEST_SHA256) - if (Build.VERSION.SDK_INT >= 28) { - setIsStrongBoxBacked(isStrongBoxSupported) - } - } - ), - } - - private fun delete() { - androidKeystore.deleteEntry(KEYSTORE_ALIAS) - // Remove Tink key set used by AndroidX's EncryptedFile. - context.getSharedPreferences(ANDROIDX_SECURITY_KEYSET_PREF_NAME, Context.MODE_PRIVATE).edit { - clear() - } - if (privateKeyFile.isFile) { - privateKeyFile.delete() - } - if (publicKeyFile.isFile) { - publicKeyFile.delete() - } - context.getEncryptedGitPrefs().edit { remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) } - type = null - } - - fun import(uri: Uri) { - // First check whether the content at uri is likely an SSH private key. - val fileSize = - context.contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)?.use { - cursor -> - // Cursor returns only a single row. - cursor.moveToFirst() - cursor.getInt(0) - } - ?: throw IOException(context.getString(R.string.ssh_key_does_not_exist)) - - // We assume that an SSH key's ideal size is > 0 bytes && < 100 kilobytes. - if (fileSize > 100_000 || fileSize == 0) - throw IllegalArgumentException( - context.getString(R.string.ssh_key_import_error_not_an_ssh_key_message) - ) - - val sshKeyInputStream = - context.contentResolver.openInputStream(uri) - ?: throw IOException(context.getString(R.string.ssh_key_does_not_exist)) - val lines = sshKeyInputStream.bufferedReader().readLines() - - // The file must have more than 2 lines, and the first and last line must have private key - // markers. - if ( - lines.size < 2 || - !Regex("BEGIN .* PRIVATE KEY").containsMatchIn(lines.first()) || - !Regex("END .* PRIVATE KEY").containsMatchIn(lines.last()) - ) - throw IllegalArgumentException( - context.getString(R.string.ssh_key_import_error_not_an_ssh_key_message) - ) - - // At this point, we are reasonably confident that we have actually been provided a private - // key and delete the old key. - delete() - // Canonicalize line endings to '\n'. - privateKeyFile.writeText(lines.joinToString("\n")) - - type = Type.Imported - } - - @Deprecated("To be used only in Migrations.kt") - fun useLegacyKey(isGenerated: Boolean) { - type = if (isGenerated) Type.LegacyGenerated else Type.Imported - } - - @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun getOrCreateWrappingMasterKey(requireAuthentication: Boolean) = - withContext(Dispatchers.IO) { - MasterKey.Builder(context, KEYSTORE_ALIAS).run { - setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - setRequestStrongBoxBacked(true) - setUserAuthenticationRequired(requireAuthentication, 15) - build() - } - } - - @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun getOrCreateWrappedPrivateKeyFile(requireAuthentication: Boolean) = - withContext(Dispatchers.IO) { - EncryptedFile.Builder( - context, - privateKeyFile, - getOrCreateWrappingMasterKey(requireAuthentication), - EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB - ) - .run { - setKeysetPrefName(ANDROIDX_SECURITY_KEYSET_PREF_NAME) - build() - } - } - - @Suppress("BlockingMethodInNonBlockingContext") - suspend fun generateKeystoreWrappedEd25519Key(requireAuthentication: Boolean) = - withContext(Dispatchers.IO) { - delete() - - val encryptedPrivateKeyFile = getOrCreateWrappedPrivateKeyFile(requireAuthentication) - // Generate the ed25519 key pair and encrypt the private key. - val keyPair = net.i2p.crypto.eddsa.KeyPairGenerator().generateKeyPair() - encryptedPrivateKeyFile.openFileOutput().use { os -> - os.write((keyPair.private as EdDSAPrivateKey).seed) - } - - // Write public key in SSH format to .ssh_key.pub. - publicKeyFile.writeText(toSshPublicKey(keyPair.public)) - - type = Type.KeystoreWrappedEd25519 - } - - fun generateKeystoreNativeKey(algorithm: Algorithm, requireAuthentication: Boolean) { - delete() - - // Generate Keystore-backed private key. - val parameterSpec = - KeyGenParameterSpec.Builder(KEYSTORE_ALIAS, KeyProperties.PURPOSE_SIGN).run { - apply(algorithm.applyToSpec) - if (requireAuthentication) { - setUserAuthenticationRequired(true) - if (Build.VERSION.SDK_INT >= 30) { - setUserAuthenticationParameters(30, KeyProperties.AUTH_DEVICE_CREDENTIAL) - } else { - @Suppress("DEPRECATION") setUserAuthenticationValidityDurationSeconds(30) - } - } - build() - } - val keyPair = - KeyPairGenerator.getInstance(algorithm.algorithm, PROVIDER_ANDROID_KEY_STORE).run { - initialize(parameterSpec) - generateKeyPair() - } - - // Write public key in SSH format to .ssh_key.pub. - publicKeyFile.writeText(toSshPublicKey(keyPair.public)) - - type = Type.KeystoreNative - } - - fun provide(client: SSHClient, passphraseFinder: InteractivePasswordFinder): KeyProvider? = - when (type) { - Type.LegacyGenerated, - Type.Imported -> client.loadKeys(privateKeyFile.absolutePath, passphraseFinder) - Type.KeystoreNative -> KeystoreNativeKeyProvider - Type.KeystoreWrappedEd25519 -> KeystoreWrappedEd25519KeyProvider - null -> null - } - - private object KeystoreNativeKeyProvider : KeyProvider { - - override fun getPublic(): PublicKey = - runCatching { androidKeystore.sshPublicKey!! } - .getOrElse { error -> - logcat { error.asLog() } - throw IOException( - "Failed to get public key '$KEYSTORE_ALIAS' from Android Keystore", - error - ) - } - - override fun getPrivate(): PrivateKey = - runCatching { androidKeystore.sshPrivateKey!! } - .getOrElse { error -> - logcat { error.asLog() } - throw IOException( - "Failed to access private key '$KEYSTORE_ALIAS' from Android Keystore", - error - ) - } - - override fun getType(): KeyType = KeyType.fromKey(public) - } - - private object KeystoreWrappedEd25519KeyProvider : KeyProvider { - - override fun getPublic(): PublicKey = - runCatching { parseSshPublicKey(sshPublicKey!!)!! } - .getOrElse { error -> - logcat { error.asLog() } - throw IOException("Failed to get the public key for wrapped ed25519 key", error) - } - - override fun getPrivate(): PrivateKey = - runCatching { - // The current MasterKey API does not allow getting a reference to an existing - // one - // without specifying the KeySpec for a new one. However, the value for passed - // here - // for `requireAuthentication` is not used as the key already exists at this - // point. - val encryptedPrivateKeyFile = runBlocking { getOrCreateWrappedPrivateKeyFile(false) } - val rawPrivateKey = encryptedPrivateKeyFile.openFileInput().use { it.readBytes() } - EdDSAPrivateKey( - EdDSAPrivateKeySpec(rawPrivateKey, EdDSANamedCurveTable.ED_25519_CURVE_SPEC) - ) - } - .getOrElse { error -> - logcat { error.asLog() } - throw IOException("Failed to unwrap wrapped ed25519 key", error) - } - - override fun getType(): KeyType = KeyType.fromKey(public) - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshjConfig.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshjConfig.kt deleted file mode 100644 index 948fbd35..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshjConfig.kt +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.git.sshj - -import com.github.michaelbull.result.runCatching -import com.hierynomus.sshj.key.KeyAlgorithms -import com.hierynomus.sshj.transport.cipher.BlockCiphers -import com.hierynomus.sshj.transport.cipher.GcmCiphers -import com.hierynomus.sshj.transport.kex.ExtInfoClientFactory -import com.hierynomus.sshj.transport.mac.Macs -import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile -import java.security.Security -import logcat.LogPriority.ERROR -import logcat.LogPriority.INFO -import logcat.LogPriority.VERBOSE -import logcat.LogPriority.WARN -import logcat.asLog -import logcat.logcat -import net.schmizz.keepalive.KeepAliveProvider -import net.schmizz.sshj.ConfigImpl -import net.schmizz.sshj.common.LoggerFactory -import net.schmizz.sshj.common.SecurityUtils -import net.schmizz.sshj.transport.compression.NoneCompression -import net.schmizz.sshj.transport.kex.Curve25519SHA256 -import net.schmizz.sshj.transport.kex.Curve25519SHA256.FactoryLibSsh -import net.schmizz.sshj.transport.kex.DHGexSHA256 -import net.schmizz.sshj.transport.kex.ECDHNistP -import net.schmizz.sshj.transport.random.JCERandom -import net.schmizz.sshj.transport.random.SingletonRandomFactory -import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile -import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile -import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile -import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.slf4j.Logger -import org.slf4j.Marker - -fun setUpBouncyCastleForSshj() { - // Replace the Android BC provider with the Java BouncyCastle provider since the former does - // not include all the required algorithms. - // Note: This may affect crypto operations in other parts of the application. - val bcIndex = - Security.getProviders().indexOfFirst { it.name == BouncyCastleProvider.PROVIDER_NAME } - if (bcIndex == -1) { - // No Android BC found, install Java BC at lowest priority. - Security.addProvider(BouncyCastleProvider()) - } else { - // Replace Android BC with Java BC, inserted at the same position. - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) - // May be needed on Android Pie+ as per https://stackoverflow.com/a/57897224/297261 - runCatching { Class.forName("sun.security.jca.Providers") } - Security.insertProviderAt(BouncyCastleProvider(), bcIndex + 1) - } - logcat("setUpBouncyCastleForSshj") { - "JCE providers: ${Security.getProviders().joinToString { "${it.name} (${it.version})" }}" - } - // Prevent sshj from forwarding all cryptographic operations to BC. - SecurityUtils.setRegisterBouncyCastle(false) - SecurityUtils.setSecurityProvider(null) -} - -private abstract class AbstractLogger(private val name: String) : Logger { - - abstract fun t(message: String, t: Throwable? = null, vararg args: Any?) - abstract fun d(message: String, t: Throwable? = null, vararg args: Any?) - abstract fun i(message: String, t: Throwable? = null, vararg args: Any?) - abstract fun w(message: String, t: Throwable? = null, vararg args: Any?) - abstract fun e(message: String, t: Throwable? = null, vararg args: Any?) - - override fun getName() = name - - override fun isTraceEnabled(marker: Marker?): Boolean = isTraceEnabled - override fun isDebugEnabled(marker: Marker?): Boolean = isDebugEnabled - override fun isInfoEnabled(marker: Marker?): Boolean = isInfoEnabled - override fun isWarnEnabled(marker: Marker?): Boolean = isWarnEnabled - override fun isErrorEnabled(marker: Marker?): Boolean = isErrorEnabled - - override fun trace(msg: String) = t(msg) - override fun trace(format: String, arg: Any?) = t(format, null, arg) - override fun trace(format: String, arg1: Any?, arg2: Any?) = t(format, null, arg1, arg2) - override fun trace(format: String, vararg arguments: Any?) = t(format, null, *arguments) - override fun trace(msg: String, t: Throwable?) = t(msg, t) - override fun trace(marker: Marker, msg: String) = trace(msg) - override fun trace(marker: Marker?, format: String, arg: Any?) = trace(format, arg) - override fun trace(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = - trace(format, arg1, arg2) - - override fun trace(marker: Marker?, format: String, vararg arguments: Any?) = - trace(format, *arguments) - - override fun trace(marker: Marker?, msg: String, t: Throwable?) = trace(msg, t) - - override fun debug(msg: String) = d(msg) - override fun debug(format: String, arg: Any?) = d(format, null, arg) - override fun debug(format: String, arg1: Any?, arg2: Any?) = d(format, null, arg1, arg2) - override fun debug(format: String, vararg arguments: Any?) = d(format, null, *arguments) - override fun debug(msg: String, t: Throwable?) = d(msg, t) - override fun debug(marker: Marker, msg: String) = debug(msg) - override fun debug(marker: Marker?, format: String, arg: Any?) = debug(format, arg) - override fun debug(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = - debug(format, arg1, arg2) - - override fun debug(marker: Marker?, format: String, vararg arguments: Any?) = - debug(format, *arguments) - - override fun debug(marker: Marker?, msg: String, t: Throwable?) = debug(msg, t) - - override fun info(msg: String) = i(msg) - override fun info(format: String, arg: Any?) = i(format, null, arg) - override fun info(format: String, arg1: Any?, arg2: Any?) = i(format, null, arg1, arg2) - override fun info(format: String, vararg arguments: Any?) = i(format, null, *arguments) - override fun info(msg: String, t: Throwable?) = i(msg, t) - override fun info(marker: Marker, msg: String) = info(msg) - override fun info(marker: Marker?, format: String, arg: Any?) = info(format, arg) - override fun info(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = - info(format, arg1, arg2) - - override fun info(marker: Marker?, format: String, vararg arguments: Any?) = - info(format, *arguments) - - override fun info(marker: Marker?, msg: String, t: Throwable?) = info(msg, t) - - override fun warn(msg: String) = w(msg) - override fun warn(format: String, arg: Any?) = w(format, null, arg) - override fun warn(format: String, arg1: Any?, arg2: Any?) = w(format, null, arg1, arg2) - override fun warn(format: String, vararg arguments: Any?) = w(format, null, *arguments) - override fun warn(msg: String, t: Throwable?) = w(msg, t) - override fun warn(marker: Marker, msg: String) = warn(msg) - override fun warn(marker: Marker?, format: String, arg: Any?) = warn(format, arg) - override fun warn(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = - warn(format, arg1, arg2) - - override fun warn(marker: Marker?, format: String, vararg arguments: Any?) = - warn(format, *arguments) - - override fun warn(marker: Marker?, msg: String, t: Throwable?) = warn(msg, t) - - override fun error(msg: String) = e(msg) - override fun error(format: String, arg: Any?) = e(format, null, arg) - override fun error(format: String, arg1: Any?, arg2: Any?) = e(format, null, arg1, arg2) - override fun error(format: String, vararg arguments: Any?) = e(format, null, *arguments) - override fun error(msg: String, t: Throwable?) = e(msg, t) - override fun error(marker: Marker, msg: String) = error(msg) - override fun error(marker: Marker?, format: String, arg: Any?) = error(format, arg) - override fun error(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = - error(format, arg1, arg2) - - override fun error(marker: Marker?, format: String, vararg arguments: Any?) = - error(format, *arguments) - - override fun error(marker: Marker?, msg: String, t: Throwable?) = error(msg, t) -} - -object LogcatLoggerFactory : LoggerFactory { - private class LogcatLogger(name: String) : AbstractLogger(name) { - - override fun isTraceEnabled() = true - override fun isDebugEnabled() = true - override fun isInfoEnabled() = true - override fun isWarnEnabled() = true - override fun isErrorEnabled() = true - - // Replace slf4j's "{}" format string style with standard Java's "%s". - // The supposedly redundant escape on the } is not redundant. - @Suppress("RegExpRedundantEscape") - private fun String.fix() = replace("""(?!<\\)\{\}""".toRegex(), "%s") - - override fun t(message: String, t: Throwable?, vararg args: Any?) { - logcat(name, VERBOSE) { message.fix().format(*args) + (t?.asLog() ?: "") } - } - - override fun d(message: String, t: Throwable?, vararg args: Any?) { - logcat(name) { message.fix().format(*args) + (t?.asLog() ?: "") } - } - - override fun i(message: String, t: Throwable?, vararg args: Any?) { - logcat(name, INFO) { message.fix().format(*args) + (t?.asLog() ?: "") } - } - - override fun w(message: String, t: Throwable?, vararg args: Any?) { - logcat(name, WARN) { message.fix().format(*args) + (t?.asLog() ?: "") } - } - - override fun e(message: String, t: Throwable?, vararg args: Any?) { - logcat(name, ERROR) { message.fix().format(*args) + (t?.asLog() ?: "") } - } - } - - override fun getLogger(name: String): Logger { - return LogcatLogger(name) - } - - override fun getLogger(clazz: Class<*>): Logger { - return LogcatLogger(clazz.name) - } -} - -class SshjConfig : ConfigImpl() { - - init { - loggerFactory = LogcatLoggerFactory - keepAliveProvider = KeepAliveProvider.HEARTBEAT - version = "OpenSSH_8.2p1 Ubuntu-4ubuntu0.1" - - initKeyExchangeFactories() - initKeyAlgorithms() - initRandomFactory() - initFileKeyProviderFactories() - initCipherFactories() - initCompressionFactories() - initMACFactories() - } - - private fun initKeyExchangeFactories() { - keyExchangeFactories = - listOf( - Curve25519SHA256.Factory(), - FactoryLibSsh(), - ECDHNistP.Factory521(), - ECDHNistP.Factory384(), - ECDHNistP.Factory256(), - DHGexSHA256.Factory(), - // Sends "ext-info-c" with the list of key exchange algorithms. This is needed to - // get - // rsa-sha2-* key types to work with some servers (e.g. GitHub). - ExtInfoClientFactory(), - ) - } - - private fun initKeyAlgorithms() { - keyAlgorithms = - listOf( - KeyAlgorithms.SSHRSACertV01(), - KeyAlgorithms.EdDSA25519(), - KeyAlgorithms.ECDSASHANistp521(), - KeyAlgorithms.ECDSASHANistp384(), - KeyAlgorithms.ECDSASHANistp256(), - KeyAlgorithms.RSASHA512(), - KeyAlgorithms.RSASHA256(), - KeyAlgorithms.SSHRSA(), - ) - .map { OpenKeychainWrappedKeyAlgorithmFactory(it) } - } - - private fun initRandomFactory() { - randomFactory = SingletonRandomFactory(JCERandom.Factory()) - } - - private fun initFileKeyProviderFactories() { - fileKeyProviderFactories = - listOf( - OpenSSHKeyV1KeyFile.Factory(), - PKCS8KeyFile.Factory(), - PKCS5KeyFile.Factory(), - OpenSSHKeyFile.Factory(), - PuTTYKeyFile.Factory(), - ) - } - - private fun initCipherFactories() { - cipherFactories = - listOf( - GcmCiphers.AES128GCM(), - GcmCiphers.AES256GCM(), - BlockCiphers.AES256CTR(), - BlockCiphers.AES192CTR(), - BlockCiphers.AES128CTR(), - ) - } - - private fun initMACFactories() { - macFactories = - listOf( - Macs.HMACSHA2512Etm(), - Macs.HMACSHA2256Etm(), - Macs.HMACSHA2512(), - Macs.HMACSHA2256(), - ) - } - - private fun initCompressionFactories() { - compressionFactories = - listOf( - NoneCompression.Factory(), - ) - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshjSessionFactory.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshjSessionFactory.kt deleted file mode 100644 index a9f84fa9..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshjSessionFactory.kt +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.git.sshj - -import android.util.Base64 -import com.github.michaelbull.result.getOrElse -import com.github.michaelbull.result.runCatching -import dev.msfjarvis.aps.util.git.operation.CredentialFinder -import dev.msfjarvis.aps.util.settings.AuthMode -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.security.PublicKey -import java.util.Collections -import java.util.concurrent.TimeUnit -import kotlin.coroutines.Continuation -import kotlin.coroutines.suspendCoroutine -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import logcat.LogPriority.WARN -import logcat.logcat -import net.schmizz.sshj.SSHClient -import net.schmizz.sshj.common.Buffer.PlainBuffer -import net.schmizz.sshj.common.DisconnectReason -import net.schmizz.sshj.common.SSHException -import net.schmizz.sshj.common.SSHRuntimeException -import net.schmizz.sshj.common.SecurityUtils -import net.schmizz.sshj.connection.channel.direct.Session -import net.schmizz.sshj.transport.verification.FingerprintVerifier -import net.schmizz.sshj.transport.verification.HostKeyVerifier -import net.schmizz.sshj.userauth.method.AuthPassword -import net.schmizz.sshj.userauth.method.AuthPublickey -import net.schmizz.sshj.userauth.password.PasswordFinder -import net.schmizz.sshj.userauth.password.Resource -import org.eclipse.jgit.transport.CredentialsProvider -import org.eclipse.jgit.transport.RemoteSession -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) -} - -abstract class InteractivePasswordFinder : PasswordFinder { - - private var isRetry = false - - abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) - - final override fun reqPassword(resource: Resource<*>?): CharArray { - val password = - runBlocking(Dispatchers.Main) { - suspendCoroutine<String?> { cont -> askForPassword(cont, isRetry) } - } - isRetry = true - return password?.toCharArray() ?: throw SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER) - } - - final override fun shouldRetry(resource: Resource<*>?) = true -} - -class SshjSessionFactory(private val authMethod: SshAuthMethod, private val hostKeyFile: File) : - SshSessionFactory() { - - private var currentSession: SshjSession? = null - - override fun getSession( - uri: URIish, - credentialsProvider: CredentialsProvider?, - fs: FS?, - tms: Int - ): RemoteSession { - return currentSession - ?: SshjSession(uri, uri.user, authMethod, hostKeyFile).connect().also { - logcat { "New SSH connection created" } - currentSession = it - } - } - - fun close() { - currentSession?.close() - } -} - -private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier { - if (!hostKeyFile.exists()) { - return object : HostKeyVerifier { - override fun verify(hostname: String?, port: Int, key: PublicKey?): Boolean { - val digest = - runCatching { SecurityUtils.getMessageDigest("SHA-256") } - .getOrElse { e -> throw SSHRuntimeException(e) } - digest.update(PlainBuffer().putPublicKey(key).compactData) - val digestData = digest.digest() - val hostKeyEntry = "SHA256:${Base64.encodeToString(digestData, Base64.NO_WRAP)}" - logcat(SshjSessionFactory::class.java.simpleName) { - "Trusting host key on first use: $hostKeyEntry" - } - hostKeyFile.writeText(hostKeyEntry) - return true - } - - override fun findExistingAlgorithms(hostname: String?, port: Int): MutableList<String> { - return Collections.emptyList() - } - } - } else { - val hostKeyEntry = hostKeyFile.readText() - logcat(SshjSessionFactory::class.java.simpleName) { "Pinned host key: $hostKeyEntry" } - return FingerprintVerifier.getInstance(hostKeyEntry) - } -} - -private class SshjSession( - uri: URIish, - private val username: String, - private val authMethod: SshAuthMethod, - private val hostKeyFile: File -) : RemoteSession { - - private lateinit var ssh: SSHClient - private var currentCommand: Session? = null - - private val uri = - if (uri.host.contains('@')) { - // URIish's String constructor cannot handle '@' in the user part of the URI and the URL - // constructor can't be used since Java's URL does not recognize the ssh scheme. We thus - // need to patch everything up ourselves. - logcat { "Before fixup: user=${uri.user}, host=${uri.host}" } - val userPlusHost = "${uri.user}@${uri.host}" - val realUser = userPlusHost.substringBeforeLast('@') - val realHost = userPlusHost.substringAfterLast('@') - uri.setUser(realUser).setHost(realHost).also { - logcat { "After fixup: user=${it.user}, host=${it.host}" } - } - } else { - uri - } - - fun connect(): SshjSession { - ssh = SSHClient(SshjConfig()) - ssh.addHostKeyVerifier(makeTofuHostKeyVerifier(hostKeyFile)) - ssh.connect(uri.host, uri.port.takeUnless { it == -1 } ?: 22) - if (!ssh.isConnected) throw IOException() - val passwordAuth = AuthPassword(CredentialFinder(authMethod.activity, AuthMode.Password)) - when (authMethod) { - is SshAuthMethod.Password -> { - ssh.auth(username, passwordAuth) - } - is SshAuthMethod.SshKey -> { - val pubkeyAuth = - 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 - } - - override fun exec(commandName: String?, timeout: Int): Process { - if (currentCommand != null) { - logcat(WARN) { "Killing old command" } - disconnect() - } - val session = ssh.startSession() - currentCommand = session - return SshjProcess(session.exec(commandName), timeout.toLong()) - } - - /** - * Kills the current command if one is running and returns the session into a state where `exec` - * can be called. - * - * Note that this does *not* disconnect the session. Unfortunately, the function has to be called - * `disconnect` to override the corresponding abstract function in `RemoteSession`. - */ - override fun disconnect() { - currentCommand?.close() - currentCommand = null - } - - fun close() { - disconnect() - ssh.close() - } -} - -private class SshjProcess(private val command: Session.Command, private val timeout: Long) : - Process() { - - override fun waitFor(): Int { - command.join(timeout, TimeUnit.SECONDS) - command.close() - return exitValue() - } - - override fun destroy() = command.close() - - override fun getOutputStream(): OutputStream = command.outputStream - - override fun getErrorStream(): InputStream = command.errorStream - - override fun exitValue(): Int = command.exitStatus - - override fun getInputStream(): InputStream = command.inputStream -} |
