diff options
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git')
7 files changed, 394 insertions, 31 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/ErrorMessages.kt b/app/src/main/java/com/zeapo/pwdstore/git/ErrorMessages.kt index 990cc670..89676c7a 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/ErrorMessages.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/ErrorMessages.kt @@ -19,6 +19,7 @@ sealed class GitException(@StringRes res: Int, vararg fmt: String) : Exception(b override val message = super.message!! companion object { + private fun buildMessage(@StringRes res: Int, vararg fmt: String) = Application.instance.resources.getString(res, *fmt) } @@ -26,6 +27,7 @@ sealed class GitException(@StringRes res: Int, vararg fmt: String) : Exception(b * 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_fail_error) } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt index bf35e5c7..d5956b1f 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt @@ -15,8 +15,8 @@ import com.zeapo.pwdstore.R import com.zeapo.pwdstore.git.GitException.PullException import com.zeapo.pwdstore.git.GitException.PushException import com.zeapo.pwdstore.git.config.GitSettings -import com.zeapo.pwdstore.git.sshj.SshjSessionFactory import com.zeapo.pwdstore.git.operation.GitOperation +import com.zeapo.pwdstore.git.sshj.SshjSessionFactory import com.zeapo.pwdstore.utils.Result import com.zeapo.pwdstore.utils.snackbar import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt index 806bbb7c..1b47d79b 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt @@ -5,6 +5,7 @@ package com.zeapo.pwdstore.git.operation import android.content.Intent +import android.widget.Toast import androidx.annotation.CallSuper import androidx.core.content.edit import androidx.fragment.app.FragmentActivity @@ -17,12 +18,18 @@ import com.zeapo.pwdstore.git.config.AuthMode import com.zeapo.pwdstore.git.config.GitSettings import com.zeapo.pwdstore.git.sshj.InteractivePasswordFinder import com.zeapo.pwdstore.git.sshj.SshAuthData +import com.zeapo.pwdstore.git.sshj.SshKey import com.zeapo.pwdstore.git.sshj.SshjSessionFactory +import com.zeapo.pwdstore.utils.BiometricAuthenticator import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.getEncryptedPrefs import com.zeapo.pwdstore.utils.sharedPrefs import java.io.File +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import net.schmizz.sshj.userauth.password.PasswordFinder import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.GitCommand @@ -33,6 +40,8 @@ import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.SshSessionFactory import org.eclipse.jgit.transport.URIish +const val ANDROID_KEYSTORE_ALIAS_SSH_KEY = "ssh_key" + /** * Creates a new git operation * @@ -43,7 +52,6 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment abstract val commands: Array<GitCommand<out Any>> private var provider: CredentialsProvider? = null - private val sshKeyFile = callingActivity.filesDir.resolve(".ssh_key") private val hostKeyFile = callingActivity.filesDir.resolve(".host_key") protected var finishFromErrorDialog = true protected val repository = PasswordRepository.getRepository(gitDir) @@ -61,9 +69,10 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment when (item) { is CredentialItem.Username -> item.value = uri?.user is CredentialItem.Password -> { - item.value = cachedPassword?.clone() ?: passwordFinder.reqPassword(null).also { - cachedPassword = it.clone() - } + item.value = cachedPassword?.clone() + ?: passwordFinder.reqPassword(null).also { + cachedPassword = it.clone() + } } else -> UnsupportedCredentialItem(uri, item.javaClass.name) } @@ -88,8 +97,8 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment return this } - private fun withPublicKeyAuthentication(passphraseFinder: InteractivePasswordFinder): GitOperation { - val sessionFactory = SshjSessionFactory(SshAuthData.PublicKeyFile(sshKeyFile, passphraseFinder), hostKeyFile) + private fun withSshKeyAuthentication(passphraseFinder: InteractivePasswordFinder): GitOperation { + val sessionFactory = SshjSessionFactory(SshAuthData.SshKey(passphraseFinder), hostKeyFile) SshSessionFactory.setInstance(sessionFactory) this.provider = null return this @@ -126,27 +135,58 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment */ abstract suspend fun execute() + 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, ) { when (authMode) { - AuthMode.SshKey -> if (!sshKeyFile.exists()) { - MaterialAlertDialogBuilder(callingActivity) - .setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text)) - .setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title)) - .setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ -> - getSshKey(false) + 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) { + if (it !is BiometricAuthenticator.Result.Failure) + cont.resume(it) + } + } } - .setNegativeButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)) { _, _ -> - getSshKey(true) + when (result) { + is BiometricAuthenticator.Result.Success -> { + withSshKeyAuthentication(CredentialFinder(callingActivity, authMode)).execute() + } + is BiometricAuthenticator.Result.Cancelled -> callingActivity.finish() + is BiometricAuthenticator.Result.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() + } } - .setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> - // Finish the blank GitActivity so user doesn't have to press back - callingActivity.finish() - }.show() + } else { + withSshKeyAuthentication(CredentialFinder(callingActivity, authMode)).execute() + } } else { - withPublicKeyAuthentication( - CredentialFinder(callingActivity, authMode)).execute() + onMissingSshKeyFile() } AuthMode.OpenKeychain -> withOpenKeychainAuthentication(callingActivity).execute() AuthMode.Password -> withPasswordAuthentication( diff --git a/app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainKeyProvider.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainKeyProvider.kt index cecf7505..5cb1b006 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainKeyProvider.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainKeyProvider.kt @@ -22,8 +22,6 @@ import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import net.schmizz.sshj.common.Base64 -import net.schmizz.sshj.common.Buffer import net.schmizz.sshj.common.DisconnectReason import net.schmizz.sshj.common.KeyType import net.schmizz.sshj.userauth.UserAuthException @@ -46,7 +44,7 @@ class OpenKeychainKeyProvider private constructor(private val activity: Fragment companion object { suspend fun prepareAndUse(activity: FragmentActivity, block: (provider: OpenKeychainKeyProvider) -> Unit) { - withContext(Dispatchers.Main){ + withContext(Dispatchers.Main) { OpenKeychainKeyProvider(activity) }.prepareAndUse(block) } @@ -118,10 +116,8 @@ class OpenKeychainKeyProvider private constructor(private val activity: Fragment is ApiResponse.Success -> { val response = sshPublicKeyResponse.response as SshPublicKeyResponse val sshPublicKey = response.sshPublicKey!! - val sshKeyParts = sshPublicKey.split("""\s+""".toRegex()) - check(sshKeyParts.size >= 2) { "OpenKeychain API returned invalid SSH key" } - @Suppress("BlockingMethodInNonBlockingContext") - publicKey = Buffer.PlainBuffer(Base64.decode(sshKeyParts[1])).readPublicKey() + publicKey = parseSshPublicKey(sshPublicKey) + ?: throw IllegalStateException("OpenKeychain API returned invalid SSH key") } is ApiResponse.NoSuchKey -> if (isRetry) { throw sshPublicKeyResponse.exception diff --git a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt new file mode 100644 index 00000000..8b657040 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt @@ -0,0 +1,321 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.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.ajalt.timberkt.d +import com.github.ajalt.timberkt.e +import com.zeapo.pwdstore.Application +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.utils.PreferenceKeys +import com.zeapo.pwdstore.utils.getEncryptedPrefs +import com.zeapo.pwdstore.utils.getString +import com.zeapo.pwdstore.utils.sharedPrefs +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 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 lazy { + 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.KeystoreNative, Type.KeystoreWrappedEd25519) + val exists + get() = type != null + val mustAuthenticate: Boolean + get() { + return try { + 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") + } + } catch (error: Exception) { + // 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. + d(error) + 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 lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + 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"), + ; + + 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 >= Build.VERSION_CODES.P) { + 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.getEncryptedPrefs("git_operation").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 + } + + @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) + 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.Imported -> client.loadKeys(privateKeyFile.absolutePath, passphraseFinder) + Type.KeystoreNative -> KeystoreNativeKeyProvider + Type.KeystoreWrappedEd25519 -> KeystoreWrappedEd25519KeyProvider + null -> null + } + + private object KeystoreNativeKeyProvider : KeyProvider { + + override fun getPublic(): PublicKey = try { + androidKeystore.sshPublicKey!! + } catch (error: Throwable) { + e(error) + throw IOException("Failed to get public key '$KEYSTORE_ALIAS' from Android Keystore", error) + } + + override fun getPrivate(): PrivateKey = try { + androidKeystore.sshPrivateKey!! + } catch (error: Throwable) { + e(error) + 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 = try { + parseSshPublicKey(sshPublicKey!!)!! + } catch (error: Throwable) { + e(error) + throw IOException("Failed to get the public key for wrapped ed25519 key", error) + } + + override fun getPrivate(): PrivateKey = try { + // 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)) + } catch (error: Throwable) { + e(error) + throw IOException("Failed to unwrap wrapped ed25519 key", error) + } + + override fun getType(): KeyType = KeyType.fromKey(public) + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt index bf454cd5..0cd5459b 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt @@ -15,6 +15,7 @@ import java.security.Security 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 @@ -52,6 +53,9 @@ fun setUpBouncyCastleForSshj() { Security.insertProviderAt(BouncyCastleProvider(), bcIndex + 1) } d { "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 { diff --git a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjSessionFactory.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjSessionFactory.kt index 05428e41..58002af0 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjSessionFactory.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjSessionFactory.kt @@ -37,7 +37,7 @@ import org.eclipse.jgit.util.FS sealed class SshAuthData { class Password(val passwordFinder: InteractivePasswordFinder) : SshAuthData() - class PublicKeyFile(val keyFile: File, val passphraseFinder: InteractivePasswordFinder) : SshAuthData() + class SshKey(val passphraseFinder: InteractivePasswordFinder) : SshAuthData() class OpenKeychain(val activity: FragmentActivity) : SshAuthData() } @@ -127,8 +127,8 @@ private class SshjSession(uri: URIish, private val username: String, private val is SshAuthData.Password -> { ssh.authPassword(username, authData.passwordFinder) } - is SshAuthData.PublicKeyFile -> { - ssh.authPublickey(username, ssh.loadKeys(authData.keyFile.absolutePath, authData.passphraseFinder)) + is SshAuthData.SshKey -> { + ssh.authPublickey(username, SshKey.provide(ssh, authData.passphraseFinder)) } is SshAuthData.OpenKeychain -> { runBlocking { |
