diff options
| author | Fabian Henneke <FabianHenneke@users.noreply.github.com> | 2020-05-28 06:27:30 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-05-28 09:57:30 +0530 |
| commit | 97911c587754a7c11e594004b77c4ac53af8a468 (patch) | |
| tree | 680453c63fc3873139a3937268f73ec6985cf03a /app/src/main/java/com/zeapo/pwdstore/git/config | |
| parent | fea82fed47d87138a9a24eb0358fc9a917474dc7 (diff) | |
| download | HardenedPass-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.kt | 58 | ||||
| -rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt | 148 |
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 +} |
