aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/git/config
diff options
context:
space:
mode:
authorFabian Henneke <FabianHenneke@users.noreply.github.com>2020-05-28 06:27:30 +0200
committerGitHub <noreply@github.com>2020-05-28 09:57:30 +0530
commit97911c587754a7c11e594004b77c4ac53af8a468 (patch)
tree680453c63fc3873139a3937268f73ec6985cf03a /app/src/main/java/com/zeapo/pwdstore/git/config
parentfea82fed47d87138a9a24eb0358fc9a917474dc7 (diff)
downloadHardenedPass-97911c587754a7c11e594004b77c4ac53af8a468.tar.gz
HardenedPass-97911c587754a7c11e594004b77c4ac53af8a468.zip
Use SSHJ for SSH public key authentication (#801)
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git/config')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/config/SshConfigSessionFactory.kt58
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt148
2 files changed, 148 insertions, 58 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshConfigSessionFactory.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/SshConfigSessionFactory.kt
deleted file mode 100644
index 9be49345..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshConfigSessionFactory.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package com.zeapo.pwdstore.git.config
-
-import com.jcraft.jsch.JSch
-import com.jcraft.jsch.JSchException
-import com.jcraft.jsch.Session
-import org.eclipse.jgit.errors.UnsupportedCredentialItem
-import org.eclipse.jgit.transport.CredentialItem
-import org.eclipse.jgit.transport.CredentialsProvider
-import org.eclipse.jgit.transport.CredentialsProviderUserInfo
-import org.eclipse.jgit.transport.OpenSshConfig
-import org.eclipse.jgit.transport.URIish
-import org.eclipse.jgit.util.FS
-
-class SshConfigSessionFactory(private val sshKey: String, private val username: String, private val passphrase: String) : GitConfigSessionFactory() {
-
- @Throws(JSchException::class)
- override fun getJSch(hc: OpenSshConfig.Host, fs: FS): JSch {
- val jsch = super.getJSch(hc, fs)
- jsch.removeAllIdentity()
- jsch.addIdentity(sshKey)
- return jsch
- }
-
- override fun configure(hc: OpenSshConfig.Host, session: Session) {
- session.setConfig("StrictHostKeyChecking", "no")
- session.setConfig("PreferredAuthentications", "publickey,password")
-
- val provider = object : CredentialsProvider() {
- override fun isInteractive(): Boolean {
- return false
- }
-
- override fun supports(vararg items: CredentialItem): Boolean {
- return true
- }
-
- @Throws(UnsupportedCredentialItem::class)
- override fun get(uri: URIish, vararg items: CredentialItem): Boolean {
- for (item in items) {
- if (item is CredentialItem.Username) {
- item.value = username
- continue
- }
- if (item is CredentialItem.StringType) {
- item.value = passphrase
- }
- }
- return true
- }
- }
- val userInfo = CredentialsProviderUserInfo(session, provider)
- session.userInfo = userInfo
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt
new file mode 100644
index 00000000..5287989a
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.git.config
+
+import android.util.Base64
+import com.github.ajalt.timberkt.d
+import com.github.ajalt.timberkt.w
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import net.schmizz.sshj.SSHClient
+import net.schmizz.sshj.common.Buffer.PlainBuffer
+import net.schmizz.sshj.common.SSHRuntimeException
+import net.schmizz.sshj.common.SecurityUtils
+import net.schmizz.sshj.connection.channel.direct.Session
+import net.schmizz.sshj.transport.verification.FingerprintVerifier
+import net.schmizz.sshj.transport.verification.HostKeyVerifier
+import net.schmizz.sshj.userauth.password.PasswordFinder
+import net.schmizz.sshj.userauth.password.Resource
+import org.eclipse.jgit.transport.CredentialsProvider
+import org.eclipse.jgit.transport.RemoteSession
+import org.eclipse.jgit.transport.SshSessionFactory
+import org.eclipse.jgit.transport.URIish
+import org.eclipse.jgit.util.FS
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.security.GeneralSecurityException
+import java.util.concurrent.TimeUnit
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.suspendCoroutine
+
+sealed class SshAuthData {
+ class Password(val passwordFinder: InteractivePasswordFinder) : SshAuthData()
+ class PublicKeyFile(val keyFile: File, val passphraseFinder: InteractivePasswordFinder) : SshAuthData()
+}
+
+class InteractivePasswordFinder(val askForPassword: (cont: Continuation<String?>, isRetry: Boolean) -> Unit) : PasswordFinder {
+
+ private var isRetry = false
+ private var shouldRetry = true
+
+ override fun reqPassword(resource: Resource<*>?): CharArray {
+ val password = runBlocking(Dispatchers.Main) {
+ suspendCoroutine<String?> { cont ->
+ askForPassword(cont, isRetry)
+ }
+ }
+ isRetry = true
+ return if (password != null) {
+ password.toCharArray()
+ } else {
+ shouldRetry = false
+ CharArray(0)
+ }
+ }
+
+ override fun shouldRetry(resource: Resource<*>?) = shouldRetry
+}
+
+class SshjSessionFactory(private val username: String, private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() {
+
+ override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession {
+ return SshjSession(uri, username, authData, hostKeyFile).connect()
+ }
+}
+
+private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier {
+ if (!hostKeyFile.exists()) {
+ return HostKeyVerifier { _, _, key ->
+ val digest = try {
+ SecurityUtils.getMessageDigest("SHA-256")
+ } catch (e: GeneralSecurityException) {
+ 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(private val uri: URIish, private val username: String, private val authData: SshAuthData, private val hostKeyFile: File) : RemoteSession {
+
+ private lateinit var ssh: SSHClient
+ private var currentCommand: Session? = null
+
+ fun connect(): SshjSession {
+ ssh = SSHClient()
+ ssh.addHostKeyVerifier(makeTofuHostKeyVerifier(hostKeyFile))
+ ssh.connect(uri.host, uri.port.takeUnless { it == -1 } ?: 22)
+ if (!ssh.isConnected)
+ throw IOException()
+ when (authData) {
+ is SshAuthData.Password -> {
+ ssh.authPassword(username, authData.passwordFinder)
+ }
+ is SshAuthData.PublicKeyFile -> {
+ ssh.authPublickey(username, ssh.loadKeys(authData.keyFile.absolutePath, authData.passphraseFinder))
+ }
+ }
+ return this
+ }
+
+ override fun exec(commandName: String?, timeout: Int): Process {
+ if (currentCommand != null) {
+ w { "Killing old session" }
+ currentCommand?.close()
+ currentCommand = null
+ }
+ val session = ssh.startSession()
+ currentCommand = session
+ return SshjProcess(session.exec(commandName), timeout.toLong())
+ }
+
+ override fun disconnect() {
+ currentCommand?.close()
+ ssh.close()
+ }
+}
+
+private class SshjProcess(private val command: Session.Command, private val timeout: Long) : Process() {
+
+ override fun waitFor(): Int {
+ command.join(timeout, TimeUnit.SECONDS)
+ command.close()
+ return exitValue()
+ }
+
+ override fun destroy() = command.close()
+
+ override fun getOutputStream(): OutputStream = command.outputStream
+
+ override fun getErrorStream(): InputStream = command.errorStream
+
+ override fun exitValue(): Int = command.exitStatus
+
+ override fun getInputStream(): InputStream = command.inputStream
+}