Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple authentication methods #825

Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
- Add [Bromite](https://www.bromite.org/) and [Ungoogled Chromium](https://git.droidware.info/wchen342/ungoogled-chromium-android) to supported browsers list for Autofill
- Add ability to view the Git commit log
- Allow generating ECDSA and ED25519 keys for SSH
- Add support for multiple/fallback authentication methods for SSH

### Changed

Expand Down
20 changes: 8 additions & 12 deletions app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt
Expand Up @@ -19,7 +19,7 @@ import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.GitCommandExecutor
import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.sshj.SshAuthData
import com.zeapo.pwdstore.git.sshj.SshAuthMethod
import com.zeapo.pwdstore.git.sshj.SshKey
import com.zeapo.pwdstore.git.sshj.SshjSessionFactory
import com.zeapo.pwdstore.utils.BiometricAuthenticator
Expand Down Expand Up @@ -99,8 +99,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
}
}

private fun registerAuthProviders(authData: SshAuthData, credentialsProvider: CredentialsProvider? = null) {
sshSessionFactory = SshjSessionFactory(authData, hostKeyFile)
private fun registerAuthProviders(authMethod: SshAuthMethod, credentialsProvider: CredentialsProvider? = null) {
sshSessionFactory = SshjSessionFactory(authMethod, hostKeyFile)
commands.filterIsInstance<TransportCommand<*, *>>().forEach { command ->
command.setTransportConfigCallback { transport: Transport ->
(transport as? SshTransport)?.sshSessionFactory = sshSessionFactory
Expand Down Expand Up @@ -154,8 +154,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
}
when (result) {
is BiometricAuthenticator.Result.Success -> {
registerAuthProviders(
SshAuthData.SshKey(CredentialFinder(callingActivity, AuthMode.SshKey)))
registerAuthProviders(SshAuthMethod.SshKey(callingActivity))
}
is BiometricAuthenticator.Result.Cancelled -> {
return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER))
Expand All @@ -173,21 +172,18 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
}
}
} else {
registerAuthProviders(SshAuthData.SshKey(CredentialFinder(callingActivity, AuthMode.SshKey)))
registerAuthProviders(SshAuthMethod.SshKey(callingActivity))
}
} else {
onMissingSshKeyFile()
// This would correctly cancel the operation but won't surface a user-visible
// error, allowing users to make the SSH key selection.
return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER))
}
AuthMode.OpenKeychain -> registerAuthProviders(SshAuthData.OpenKeychain(callingActivity))
AuthMode.OpenKeychain -> registerAuthProviders(SshAuthMethod.OpenKeychain(callingActivity))
AuthMode.Password -> {
val credentialFinder = CredentialFinder(callingActivity, AuthMode.Password)
val httpsCredentialProvider = HttpsCredentialsProvider(credentialFinder)
registerAuthProviders(
SshAuthData.Password(CredentialFinder(callingActivity, AuthMode.Password)),
httpsCredentialProvider)
val httpsCredentialProvider = HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password))
registerAuthProviders(SshAuthMethod.Password(callingActivity), httpsCredentialProvider)
}
AuthMode.None -> {
}
Expand Down
Expand Up @@ -39,7 +39,7 @@ 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(private val activity: FragmentActivity) : KeyProvider, Closeable {
class OpenKeychainKeyProvider private constructor(activity: FragmentActivity) : KeyProvider, Closeable {

companion object {

Expand Down
37 changes: 22 additions & 15 deletions app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjSessionFactory.kt
Expand Up @@ -10,6 +10,8 @@ import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.w
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.runCatching
import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.operation.CredentialFinder
import java.io.File
import java.io.IOException
import java.io.InputStream
Expand All @@ -28,6 +30,8 @@ import net.schmizz.sshj.common.SecurityUtils
import net.schmizz.sshj.connection.channel.direct.Session
import net.schmizz.sshj.transport.verification.FingerprintVerifier
import net.schmizz.sshj.transport.verification.HostKeyVerifier
import net.schmizz.sshj.userauth.method.AuthPassword
import net.schmizz.sshj.userauth.method.AuthPublickey
import net.schmizz.sshj.userauth.password.PasswordFinder
import net.schmizz.sshj.userauth.password.Resource
import org.eclipse.jgit.transport.CredentialsProvider
Expand All @@ -36,10 +40,10 @@ import org.eclipse.jgit.transport.SshSessionFactory
import org.eclipse.jgit.transport.URIish
import org.eclipse.jgit.util.FS

sealed class SshAuthData {
class Password(val passwordFinder: InteractivePasswordFinder) : SshAuthData()
class SshKey(val passphraseFinder: InteractivePasswordFinder) : SshAuthData()
class OpenKeychain(val activity: FragmentActivity) : SshAuthData()
sealed class SshAuthMethod(val activity: FragmentActivity) {
class Password(activity: FragmentActivity) : SshAuthMethod(activity)
class SshKey(activity: FragmentActivity) : SshAuthMethod(activity)
class OpenKeychain(activity: FragmentActivity) : SshAuthMethod(activity)
}

abstract class InteractivePasswordFinder : PasswordFinder {
Expand All @@ -62,12 +66,12 @@ abstract class InteractivePasswordFinder : PasswordFinder {
final override fun shouldRetry(resource: Resource<*>?) = true
}

class SshjSessionFactory(private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() {
class SshjSessionFactory(private val authMethod: SshAuthMethod, private val hostKeyFile: File) : SshSessionFactory() {

private var currentSession: SshjSession? = null

override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession {
return currentSession ?: SshjSession(uri, uri.user, authData, hostKeyFile).connect().also {
return currentSession ?: SshjSession(uri, uri.user, authMethod, hostKeyFile).connect().also {
d { "New SSH connection created" }
currentSession = it
}
Expand Down Expand Up @@ -100,7 +104,7 @@ private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier {
}
}

private class SshjSession(uri: URIish, private val username: String, private val authData: SshAuthData, private val hostKeyFile: File) : RemoteSession {
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
Expand All @@ -124,17 +128,20 @@ private class SshjSession(uri: URIish, private val username: String, private val
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)
val passwordAuth = AuthPassword(CredentialFinder(authMethod.activity, AuthMode.Password))
when (authMethod) {
is SshAuthMethod.Password -> {
ssh.auth(username, passwordAuth)
}
is SshAuthData.SshKey -> {
ssh.authPublickey(username, SshKey.provide(ssh, authData.passphraseFinder))
is SshAuthMethod.SshKey -> {
val pubkeyAuth = AuthPublickey(SshKey.provide(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey)))
ssh.auth(username, pubkeyAuth, passwordAuth)
}
is SshAuthData.OpenKeychain -> {
is SshAuthMethod.OpenKeychain -> {
runBlocking {
OpenKeychainKeyProvider.prepareAndUse(authData.activity) { provider ->
ssh.authPublickey(username, provider)
OpenKeychainKeyProvider.prepareAndUse(authMethod.activity) { provider ->
val openKeychainAuth = AuthPublickey(provider)
ssh.auth(username, openKeychainAuth, passwordAuth)
}
}
}
Expand Down