aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/dev/msfjarvis/aps/util/git/sshj
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/util/git/sshj')
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/git/sshj/ContinuationContainerActivity.kt27
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/git/sshj/OpenKeychainKeyProvider.kt286
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/git/sshj/OpenKeychainWrappedKeyAlgorithmFactory.kt104
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshKey.kt480
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshjConfig.kt434
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/git/sshj/SshjSessionFactory.kt235
6 files changed, 781 insertions, 785 deletions
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
index 4085c0c5..523ff5b6 100644
--- 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
@@ -14,24 +14,21 @@ import kotlin.coroutines.resumeWithException
import net.schmizz.sshj.common.DisconnectReason
import net.schmizz.sshj.userauth.UserAuthException
-/**
- * Workaround for https://msfjarvis.dev/aps/issue/1164
- */
+/** Workaround for https://msfjarvis.dev/aps/issue/1164 */
open class ContinuationContainerActivity : AppCompatActivity {
- constructor() : super()
- constructor(@LayoutRes layoutRes: Int) : super(layoutRes)
+ constructor() : super()
+ constructor(@LayoutRes layoutRes: Int) : super(layoutRes)
- var stashedCont: Continuation<Intent>? = null
+ 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))
- }
+ 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
index 23f780c3..acb7d8d7 100644
--- 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
@@ -38,162 +38,170 @@ 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 {
+class OpenKeychainKeyProvider private constructor(val activity: ContinuationContainerActivity) :
+ KeyProvider, Closeable {
- companion object {
+ companion object {
- suspend fun prepareAndUse(activity: ContinuationContainerActivity, block: (provider: OpenKeychainKeyProvider) -> Unit) {
- withContext(Dispatchers.Main) {
- OpenKeychainKeyProvider(activity)
- }.prepareAndUse(block)
- }
+ 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 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 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) {
+ d { "Bound to SshAuthenticationApi: $OPENPGP_PROVIDER" }
+ cont.resume(SshAuthenticationApi(context, sshAgent))
}
- }
- 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) {
- d { "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)
+ override fun onError() {
+ throw UserAuthException(DisconnectReason.UNKNOWN, "OpenKeychain service unavailable")
}
- 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
- }
+ if (keyId == null) {
+ selectKey()
}
-
- private suspend fun executeApiRequest(request: Request, resultOfUserInteraction: Intent? = null): ApiResponse {
- d { "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 {
- d { "executeRequest($request): $it" }
+ 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 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
- }
+ 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
}
-
- override fun close() {
- activity.lifecycleScope.launch {
- withContext(Dispatchers.Main) {
- activity.continueAfterUserInteraction.unregister()
+ }
+
+ private suspend fun executeApiRequest(request: Request, resultOfUserInteraction: Intent? = null): ApiResponse {
+ d { "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 { d { "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)
}
- sshServiceConnection.disconnect()
+ }
+ }
+ }
+
+ 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 getPrivate() = privateKey
- override fun getPublic() = publicKey
+ override fun getPublic() = publicKey
- override fun getType(): KeyType = KeyType.fromKey(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
index e64f9909..0ab01ba5 100644
--- 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
@@ -8,8 +8,6 @@ import com.hierynomus.sshj.key.KeyAlgorithm
import java.io.ByteArrayOutputStream
import java.security.PrivateKey
import java.security.interfaces.ECKey
-import java.security.interfaces.ECPrivateKey
-import java.security.spec.ECParameterSpec
import kotlinx.coroutines.runBlocking
import net.schmizz.sshj.common.Buffer
import net.schmizz.sshj.common.Factory
@@ -18,79 +16,83 @@ import org.openintents.ssh.authentication.SshAuthenticationApi
interface OpenKeychainPrivateKey : PrivateKey, ECKey {
- suspend fun sign(challenge: ByteArray, hashAlgorithm: Int): ByteArray
+ suspend fun sign(challenge: ByteArray, hashAlgorithm: Int): ByteArray
- override fun getFormat() = null
- override fun getEncoded() = null
+ override fun getFormat() = null
+ override fun getEncoded() = null
}
-class OpenKeychainWrappedKeyAlgorithmFactory(private val factory: Factory.Named<KeyAlgorithm>) : Factory.Named<KeyAlgorithm> by factory {
+class OpenKeychainWrappedKeyAlgorithmFactory(private val factory: Factory.Named<KeyAlgorithm>) :
+ Factory.Named<KeyAlgorithm> by factory {
- override fun create() = OpenKeychainWrappedKeyAlgorithm(factory.create())
+ 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
+ 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)
+ override fun newSignature() = OpenKeychainWrappedSignature(keyAlgorithm.newSignature(), hashAlgorithm)
}
-class OpenKeychainWrappedSignature(private val wrappedSignature: Signature, private val hashAlgorithm: Int) : Signature by wrappedSignature {
+class OpenKeychainWrappedSignature(private val wrappedSignature: Signature, private val hashAlgorithm: Int) :
+ Signature by wrappedSignature {
- private val data = ByteArrayOutputStream()
+ private val data = ByteArrayOutputStream()
- private var bridgedPrivateKey: OpenKeychainPrivateKey? = null
+ private var bridgedPrivateKey: OpenKeychainPrivateKey? = null
- override fun initSign(prvkey: PrivateKey?) {
- if (prvkey is OpenKeychainPrivateKey) {
- bridgedPrivateKey = prvkey
- } else {
- wrappedSignature.initSign(prvkey)
- }
+ 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?) {
+ 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 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)
- }
+ override fun sign(): ByteArray? =
+ if (bridgedPrivateKey != null) {
+ runBlocking { bridgedPrivateKey!!.sign(data.toByteArray(), hashAlgorithm) }
} else {
- wrappedSignature.sign()
+ 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()
- }
+ 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)
+ 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
index 793bbd28..37414707 100644
--- 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
@@ -51,286 +51,288 @@ private const val KEYSTORE_ALIAS = "sshkey"
private const val ANDROIDX_SECURITY_KEYSET_PREF_NAME = "androidx_sshkey_keyset_prefs"
private val androidKeystore: KeyStore by lazy(LazyThreadSafetyMode.NONE) {
- KeyStore.getInstance(PROVIDER_ANDROID_KEY_STORE).apply { load(null) }
+ KeyStore.getInstance(PROVIDER_ANDROID_KEY_STORE).apply { load(null) }
}
private val KeyStore.sshPrivateKey
- get() = getKey(KEYSTORE_ALIAS, null) as? PrivateKey
+ get() = getKey(KEYSTORE_ALIAS, null) as? PrivateKey
private val KeyStore.sshPublicKey
- get() = getCertificate(KEYSTORE_ALIAS)?.publicKey
+ 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()
+ 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)}"
+ 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.
- d(error)
- false
- }
+ 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")
}
-
- 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)
+ }
+ .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.
+ d(error)
+ false
}
-
- private val isStrongBoxSupported by lazy(LazyThreadSafetyMode.NONE) {
- 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"),
-
- // Behaves like `Imported`, but allows to view the public key.
- LegacyGenerated("legacy_generated"),
- ;
+ private val context: Context
+ get() = Application.instance.applicationContext
- companion object {
+ private val privateKeyFile
+ get() = File(context.filesDir, ".ssh_key")
+ private val publicKeyFile
+ get() = File(context.filesDir, ".ssh_key.pub")
- 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 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 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
- }
+ private val isStrongBoxSupported by lazy(LazyThreadSafetyMode.NONE) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
+ context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
+ else false
+ }
- 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))
+ private enum class Type(val value: String) {
+ Imported("imported"),
+ KeystoreNative("keystore_native"),
+ KeystoreWrappedEd25519("keystore_wrapped_ed25519"),
- // 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"))
+ // Behaves like `Imported`, but allows to view the public key.
+ LegacyGenerated("legacy_generated"),
+ ;
- type = Type.Imported
- }
+ companion object {
- @Deprecated("To be used only in Migrations.kt")
- fun useLegacyKey(isGenerated: Boolean) {
- type = if (isGenerated) Type.LegacyGenerated else Type.Imported
+ fun fromValue(value: String?): Type? = values().associateBy { it.value }[value]
}
-
- @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()
+ }
+
+ 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()
}
-
- @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()
- }
+ if (publicKeyFile.isFile) {
+ publicKeyFile.delete()
}
-
- @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
+ 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()
+ }
}
- 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 >= Build.VERSION_CODES.R) {
- 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()
+ @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()
}
-
- // 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 {
+ @Suppress("BlockingMethodInNonBlockingContext")
+ suspend fun generateKeystoreWrappedEd25519Key(requireAuthentication: Boolean) =
+ withContext(Dispatchers.IO) {
+ delete()
- override fun getPublic(): PublicKey = runCatching {
- androidKeystore.sshPublicKey!!
- }.getOrElse { error ->
- e(error)
- throw IOException("Failed to get public key '$KEYSTORE_ALIAS' from Android Keystore", error)
- }
+ 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) }
- override fun getPrivate(): PrivateKey = runCatching {
- androidKeystore.sshPrivateKey!!
- }.getOrElse { error ->
- e(error)
- throw IOException("Failed to access private key '$KEYSTORE_ALIAS' from Android Keystore", error)
- }
+ // Write public key in SSH format to .ssh_key.pub.
+ publicKeyFile.writeText(toSshPublicKey(keyPair.public))
- override fun getType(): KeyType = KeyType.fromKey(public)
+ type = Type.KeystoreWrappedEd25519
}
- private object KeystoreWrappedEd25519KeyProvider : KeyProvider {
-
- override fun getPublic(): PublicKey = runCatching {
- parseSshPublicKey(sshPublicKey!!)!!
- }.getOrElse { error ->
- e(error)
- throw IOException("Failed to get the public key for wrapped ed25519 key", error)
+ 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 >= Build.VERSION_CODES.R) {
+ 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
+ }
- 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 ->
- e(error)
- throw IOException("Failed to unwrap wrapped ed25519 key", error)
+ private object KeystoreNativeKeyProvider : KeyProvider {
+
+ override fun getPublic(): PublicKey =
+ runCatching { androidKeystore.sshPublicKey!! }.getOrElse { error ->
+ e(error)
+ throw IOException("Failed to get public key '$KEYSTORE_ALIAS' from Android Keystore", error)
+ }
+
+ override fun getPrivate(): PrivateKey =
+ runCatching { androidKeystore.sshPrivateKey!! }.getOrElse { error ->
+ 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 =
+ runCatching { parseSshPublicKey(sshPublicKey!!)!! }.getOrElse { error ->
+ e(error)
+ 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 ->
+ e(error)
+ throw IOException("Failed to unwrap wrapped ed25519 key", error)
}
- override fun getType(): KeyType = KeyType.fromKey(public)
- }
+ 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
index c3339676..e93787f4 100644
--- 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
@@ -33,250 +33,240 @@ 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)
- }
- 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)
+ // 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)
+ }
+ 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 {
- 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)
+ 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 TimberLoggerFactory : LoggerFactory {
- private class TimberLogger(name: String) : AbstractLogger(name) {
-
- // We defer the log level checks to Timber.
- 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?) {
- Timber.tag(name).v(t, message.fix(), *args)
- }
-
- override fun d(message: String, t: Throwable?, vararg args: Any?) {
- Timber.tag(name).d(t, message.fix(), *args)
- }
-
- override fun i(message: String, t: Throwable?, vararg args: Any?) {
- Timber.tag(name).i(t, message.fix(), *args)
- }
-
- override fun w(message: String, t: Throwable?, vararg args: Any?) {
- Timber.tag(name).w(t, message.fix(), *args)
- }
-
- override fun e(message: String, t: Throwable?, vararg args: Any?) {
- Timber.tag(name).e(t, message.fix(), *args)
- }
- }
+ private class TimberLogger(name: String) : AbstractLogger(name) {
- override fun getLogger(name: String): Logger {
- return TimberLogger(name)
- }
-
- override fun getLogger(clazz: Class<*>): Logger {
- return TimberLogger(clazz.name)
- }
+ // We defer the log level checks to Timber.
+ 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")
-class SshjConfig : ConfigImpl() {
-
- init {
- loggerFactory = TimberLoggerFactory
- keepAliveProvider = KeepAliveProvider.HEARTBEAT
- version = "OpenSSH_8.2p1 Ubuntu-4ubuntu0.1"
-
- initKeyExchangeFactories()
- initKeyAlgorithms()
- initRandomFactory()
- initFileKeyProviderFactories()
- initCipherFactories()
- initCompressionFactories()
- initMACFactories()
+ override fun t(message: String, t: Throwable?, vararg args: Any?) {
+ Timber.tag(name).v(t, message.fix(), *args)
}
- 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(),
- )
+ override fun d(message: String, t: Throwable?, vararg args: Any?) {
+ Timber.tag(name).d(t, message.fix(), *args)
}
- private fun initKeyAlgorithms() {
- keyAlgorithms = listOf(
- KeyAlgorithms.SSHRSACertV01(),
- KeyAlgorithms.EdDSA25519(),
- KeyAlgorithms.ECDSASHANistp521(),
- KeyAlgorithms.ECDSASHANistp384(),
- KeyAlgorithms.ECDSASHANistp256(),
- KeyAlgorithms.RSASHA512(),
- KeyAlgorithms.RSASHA256(),
- KeyAlgorithms.SSHRSA(),
- ).map {
- OpenKeychainWrappedKeyAlgorithmFactory(it)
- }
+ override fun i(message: String, t: Throwable?, vararg args: Any?) {
+ Timber.tag(name).i(t, message.fix(), *args)
}
- private fun initRandomFactory() {
- randomFactory = SingletonRandomFactory(JCERandom.Factory())
+ override fun w(message: String, t: Throwable?, vararg args: Any?) {
+ Timber.tag(name).w(t, message.fix(), *args)
}
- private fun initFileKeyProviderFactories() {
- fileKeyProviderFactories = listOf(
- OpenSSHKeyV1KeyFile.Factory(),
- PKCS8KeyFile.Factory(),
- PKCS5KeyFile.Factory(),
- OpenSSHKeyFile.Factory(),
- PuTTYKeyFile.Factory(),
- )
+ override fun e(message: String, t: Throwable?, vararg args: Any?) {
+ Timber.tag(name).e(t, message.fix(), *args)
}
+ }
+ override fun getLogger(name: String): Logger {
+ return TimberLogger(name)
+ }
- private fun initCipherFactories() {
- cipherFactories = listOf(
- GcmCiphers.AES128GCM(),
- GcmCiphers.AES256GCM(),
- BlockCiphers.AES256CTR(),
- BlockCiphers.AES192CTR(),
- BlockCiphers.AES128CTR(),
- )
- }
+ override fun getLogger(clazz: Class<*>): Logger {
+ return TimberLogger(clazz.name)
+ }
+}
- private fun initMACFactories() {
- macFactories = listOf(
- Macs.HMACSHA2512Etm(),
- Macs.HMACSHA2256Etm(),
- Macs.HMACSHA2512(),
- Macs.HMACSHA2256(),
- )
- }
+class SshjConfig : ConfigImpl() {
- private fun initCompressionFactories() {
- compressionFactories = listOf(
- NoneCompression.Factory(),
- )
- }
+ init {
+ loggerFactory = TimberLoggerFactory
+ 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
index 1062fb0e..95676d7f 100644
--- 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
@@ -40,158 +40,155 @@ 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)
+ 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
+ private var isRetry = false
- abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean)
+ 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 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
+ final override fun shouldRetry(resource: Resource<*>?) = true
}
class SshjSessionFactory(private val authMethod: SshAuthMethod, private val hostKeyFile: File) : SshSessionFactory() {
- private var currentSession: SshjSession? = null
+ 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 {
- d { "New SSH connection created" }
- currentSession = it
- }
- }
+ override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession {
+ return currentSession
+ ?: SshjSession(uri, uri.user, authMethod, hostKeyFile).connect().also {
+ d { "New SSH connection created" }
+ currentSession = it
+ }
+ }
- fun close() {
- currentSession?.close()
- }
+ fun close() {
+ currentSession?.close()
+ }
}
private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier {
- if (!hostKeyFile.exists()) {
- return HostKeyVerifier { _, _, key ->
- 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)}"
- d { "Trusting host key on first use: $hostKeyEntry" }
- hostKeyFile.writeText(hostKeyEntry)
- true
- }
- } else {
- val hostKeyEntry = hostKeyFile.readText()
- d { "Pinned host key: $hostKeyEntry" }
- return FingerprintVerifier.getInstance(hostKeyEntry)
+ if (!hostKeyFile.exists()) {
+ return HostKeyVerifier { _, _, key ->
+ 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)}"
+ d { "Trusting host key on first use: $hostKeyEntry" }
+ hostKeyFile.writeText(hostKeyEntry)
+ true
}
+ } else {
+ val hostKeyEntry = hostKeyFile.readText()
+ d { "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.
- d { "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 { d { "After fixup: user=${it.user}, host=${it.host}" } }
+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.
+ d { "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 { d { "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
+ uri
}
- override fun exec(commandName: String?, timeout: Int): Process {
- if (currentCommand != null) {
- w { "Killing old command" }
- disconnect()
+ 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)
+ }
}
- val session = ssh.startSession()
- currentCommand = session
- return SshjProcess(session.exec(commandName), timeout.toLong())
+ }
}
+ return this
+ }
- /**
- * 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()
+ override fun exec(commandName: String?, timeout: Int): Process {
+ if (currentCommand != null) {
+ w { "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 waitFor(): Int {
+ command.join(timeout, TimeUnit.SECONDS)
+ command.close()
+ return exitValue()
+ }
- override fun destroy() = command.close()
+ override fun destroy() = command.close()
- override fun getOutputStream(): OutputStream = command.outputStream
+ override fun getOutputStream(): OutputStream = command.outputStream
- override fun getErrorStream(): InputStream = command.errorStream
+ override fun getErrorStream(): InputStream = command.errorStream
- override fun exitValue(): Int = command.exitStatus
+ override fun exitValue(): Int = command.exitStatus
- override fun getInputStream(): InputStream = command.inputStream
+ override fun getInputStream(): InputStream = command.inputStream
}