Improve Git password/passphrase dialog behavior (#829)

* Reset PasswordFinder retry state after authentication

* Memorize password inbetween Git commands
This commit is contained in:
Fabian Henneke 2020-06-03 10:08:47 +02:00 committed by GitHub
parent 0c01a5bbf9
commit 4172c70c86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 51 additions and 6 deletions

View file

@ -12,6 +12,7 @@ import android.os.AsyncTask
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.zeapo.pwdstore.PasswordStore import com.zeapo.pwdstore.PasswordStore
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.config.SshjSessionFactory
import net.schmizz.sshj.userauth.UserAuthException import net.schmizz.sshj.userauth.UserAuthException
import org.eclipse.jgit.api.CommitCommand import org.eclipse.jgit.api.CommitCommand
import org.eclipse.jgit.api.GitCommand import org.eclipse.jgit.api.GitCommand
@ -147,6 +148,7 @@ class GitAsyncTask(
if (refreshListOnEnd) { if (refreshListOnEnd) {
(activity as? PasswordStore)?.resetPasswordList() (activity as? PasswordStore)?.resetPasswordList()
} }
(SshSessionFactory.getInstance() as? SshjSessionFactory)?.clearCredentials()
SshSessionFactory.setInstance(null) SshSessionFactory.setInstance(null)
} }

View file

@ -7,6 +7,7 @@ package com.zeapo.pwdstore.git.config
import android.util.Base64 import android.util.Base64
import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.w import com.github.ajalt.timberkt.w
import com.zeapo.pwdstore.utils.clear
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.schmizz.sshj.SSHClient import net.schmizz.sshj.SSHClient
@ -33,18 +34,48 @@ import kotlin.coroutines.Continuation
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
sealed class SshAuthData { sealed class SshAuthData {
class Password(val passwordFinder: InteractivePasswordFinder) : SshAuthData() class Password(val passwordFinder: InteractivePasswordFinder) : SshAuthData() {
class PublicKeyFile(val keyFile: File, val passphraseFinder: InteractivePasswordFinder) : SshAuthData() override fun clearCredentials() {
passwordFinder.clearPassword()
}
}
class PublicKeyFile(val keyFile: File, val passphraseFinder: InteractivePasswordFinder) : SshAuthData() {
override fun clearCredentials() {
passphraseFinder.clearPassword()
}
}
abstract fun clearCredentials()
} }
abstract class InteractivePasswordFinder : PasswordFinder { abstract class InteractivePasswordFinder : PasswordFinder {
private var isRetry = false
private var shouldRetry = true
abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean)
private var isRetry = false
private var shouldRetry = true
private var lastPassword: CharArray? = null
fun resetForReuse() {
isRetry = false
shouldRetry = true
}
fun clearPassword() {
lastPassword?.clear()
lastPassword = null
}
final override fun reqPassword(resource: Resource<*>?): CharArray { final override fun reqPassword(resource: Resource<*>?): CharArray {
if (lastPassword != null && !isRetry) {
// This instance successfully authenticated in a previous authentication step and is
// now being reused for a new one. We try the previous password so that the user
// does not have to type it again.
isRetry = true
return lastPassword!!
}
clearPassword()
val password = runBlocking(Dispatchers.Main) { val password = runBlocking(Dispatchers.Main) {
suspendCoroutine<String?> { cont -> suspendCoroutine<String?> { cont ->
askForPassword(cont, isRetry) askForPassword(cont, isRetry)
@ -52,7 +83,7 @@ abstract class InteractivePasswordFinder : PasswordFinder {
} }
isRetry = true isRetry = true
return if (password != null) { return if (password != null) {
password.toCharArray() password.toCharArray().also { lastPassword = it }
} else { } else {
shouldRetry = false shouldRetry = false
CharArray(0) CharArray(0)
@ -67,6 +98,10 @@ class SshjSessionFactory(private val username: String, private val authData: Ssh
override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession { override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession {
return SshjSession(uri, username, authData, hostKeyFile).connect() return SshjSession(uri, username, authData, hostKeyFile).connect()
} }
fun clearCredentials() {
authData.clearCredentials()
}
} }
private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier { private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier {
@ -105,9 +140,11 @@ private class SshjSession(private val uri: URIish, private val username: String,
when (authData) { when (authData) {
is SshAuthData.Password -> { is SshAuthData.Password -> {
ssh.authPassword(username, authData.passwordFinder) ssh.authPassword(username, authData.passwordFinder)
authData.passwordFinder.resetForReuse()
} }
is SshAuthData.PublicKeyFile -> { is SshAuthData.PublicKeyFile -> {
ssh.authPublickey(username, ssh.loadKeys(authData.keyFile.absolutePath, authData.passphraseFinder)) ssh.authPublickey(username, ssh.loadKeys(authData.keyFile.absolutePath, authData.passphraseFinder))
authData.passphraseFinder.resetForReuse()
} }
} }
return this return this

View file

@ -26,6 +26,12 @@ fun String.splitLines(): Array<String> {
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} }
fun CharArray.clear() {
forEachIndexed { i, _ ->
this[i] = 0.toChar()
}
}
fun Context.resolveAttribute(attr: Int): Int { fun Context.resolveAttribute(attr: Int): Int {
val typedValue = TypedValue() val typedValue = TypedValue()
this.theme.resolveAttribute(attr, typedValue, true) this.theme.resolveAttribute(attr, typedValue, true)