Improve Git password/passphrase dialog behavior (#829)
* Reset PasswordFinder retry state after authentication * Memorize password inbetween Git commands
This commit is contained in:
parent
0c01a5bbf9
commit
4172c70c86
3 changed files with 51 additions and 6 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue