1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
|
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.git
import android.annotation.SuppressLint
import android.content.Intent
import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.git.config.SshAuthData
import com.zeapo.pwdstore.git.config.SshjSessionFactory
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.requestInputFocusOnView
import net.schmizz.sshj.userauth.password.PasswordFinder
import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.errors.UnsupportedCredentialItem
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.transport.CredentialItem
import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.SshSessionFactory
import org.eclipse.jgit.transport.URIish
import java.io.File
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import com.google.android.material.R as materialR
private class GitOperationCredentialFinder(
val callingActivity: AppCompatActivity,
val connectionMode: ConnectionMode
) : InteractivePasswordFinder() {
override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) {
val gitOperationPrefs = callingActivity.getEncryptedPrefs("git_operation")
val credentialPref: String
@StringRes val messageRes: Int
@StringRes val hintRes: Int
@StringRes val rememberRes: Int
@StringRes val errorRes: Int
when (connectionMode) {
ConnectionMode.SshKey -> {
credentialPref = PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE
messageRes = R.string.passphrase_dialog_text
hintRes = R.string.ssh_keygen_passphrase
rememberRes = R.string.git_operation_remember_passphrase
errorRes = R.string.git_operation_wrong_passphrase
}
ConnectionMode.Password -> {
// Could be either an SSH or an HTTPS password
credentialPref = PreferenceKeys.HTTPS_PASSWORD
messageRes = R.string.password_dialog_text
hintRes = R.string.git_operation_hint_password
rememberRes = R.string.git_operation_remember_password
errorRes = R.string.git_operation_wrong_password
}
else -> throw IllegalStateException("Only SshKey and Password connection mode ask for passwords")
}
val storedCredential = gitOperationPrefs.getString(credentialPref, null)
if (isRetry)
gitOperationPrefs.edit { remove(credentialPref) }
if (storedCredential == null) {
val layoutInflater = LayoutInflater.from(callingActivity)
@SuppressLint("InflateParams")
val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential)
editCredential.setHint(hintRes)
val rememberCredential = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential)
rememberCredential.setText(rememberRes)
if (isRetry)
editCredential.setError(callingActivity.resources.getString(errorRes),
ContextCompat.getDrawable(callingActivity, materialR.drawable.mtrl_ic_error))
MaterialAlertDialogBuilder(callingActivity).run {
setTitle(R.string.passphrase_dialog_title)
setMessage(messageRes)
setView(dialogView)
setPositiveButton(R.string.dialog_ok) { _, _ ->
val credential = editCredential.text.toString()
if (rememberCredential.isChecked) {
gitOperationPrefs.edit {
putString(credentialPref, credential)
}
}
cont.resume(credential)
}
setNegativeButton(R.string.dialog_cancel) { _, _ ->
cont.resume(null)
}
setOnCancelListener {
cont.resume(null)
}
create()
}.run {
requestInputFocusOnView<TextInputEditText>(R.id.git_auth_credential)
show()
}
} else {
cont.resume(storedCredential)
}
}
}
/**
* Creates a new git operation
*
* @param gitDir the git working tree directory
* @param callingActivity the calling activity
*/
abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompatActivity) {
protected val repository: Repository? = PasswordRepository.getRepository(gitDir)
internal var provider: CredentialsProvider? = null
internal var command: GitCommand<*>? = null
private val sshKeyFile = callingActivity.filesDir.resolve(".ssh_key")
private val hostKeyFile = callingActivity.filesDir.resolve(".host_key")
private class PasswordFinderCredentialsProvider(private val username: String, private val passwordFinder: PasswordFinder) : CredentialsProvider() {
override fun isInteractive() = true
override fun get(uri: URIish?, vararg items: CredentialItem): Boolean {
for (item in items) {
when (item) {
is CredentialItem.Username -> item.value = username
is CredentialItem.Password -> item.value = passwordFinder.reqPassword(null)
else -> UnsupportedCredentialItem(uri, item.javaClass.name)
}
}
return true
}
override fun supports(vararg items: CredentialItem) = items.all {
it is CredentialItem.Username || it is CredentialItem.Password
}
}
private fun withPasswordAuthentication(username: String, passwordFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(username, SshAuthData.Password(passwordFinder), hostKeyFile)
SshSessionFactory.setInstance(sessionFactory)
this.provider = PasswordFinderCredentialsProvider(username, passwordFinder)
return this
}
private fun withPublicKeyAuthentication(username: String, passphraseFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(username, SshAuthData.PublicKeyFile(sshKeyFile, passphraseFinder), hostKeyFile)
SshSessionFactory.setInstance(sessionFactory)
this.provider = null
return this
}
private fun withOpenKeychainAuthentication(username: String, identity: SshApiSessionFactory.ApiIdentity?): GitOperation {
SshSessionFactory.setInstance(SshApiSessionFactory(username, identity))
this.provider = null
return this
}
private fun getSshKey(make: Boolean) {
try {
// Ask the UserPreference to provide us with the ssh-key
// onResult has to be handled by the callingActivity
val intent = Intent(callingActivity.applicationContext, UserPreference::class.java)
intent.putExtra("operation", if (make) "make_ssh_key" else "get_ssh_key")
callingActivity.startActivityForResult(intent, GET_SSH_KEY_FROM_CLONE)
} catch (e: Exception) {
println("Exception caught :(")
e.printStackTrace()
}
}
/**
* Executes the GitCommand in an async task
*/
abstract fun execute()
fun executeAfterAuthentication(
connectionMode: ConnectionMode,
username: String,
identity: SshApiSessionFactory.ApiIdentity?
) {
when (connectionMode) {
ConnectionMode.SshKey -> if (!sshKeyFile.exists()) {
MaterialAlertDialogBuilder(callingActivity)
.setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text))
.setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title))
.setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ ->
getSshKey(false)
}
.setNegativeButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)) { _, _ ->
getSshKey(true)
}
.setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
// Finish the blank GitActivity so user doesn't have to press back
callingActivity.finish()
}.show()
} else {
withPublicKeyAuthentication(username, GitOperationCredentialFinder(callingActivity,
connectionMode)).execute()
}
ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(username, identity).execute()
ConnectionMode.Password -> withPasswordAuthentication(
username, GitOperationCredentialFinder(callingActivity, connectionMode)).execute()
ConnectionMode.None -> execute()
}
}
/**
* Action to execute on error
*/
open fun onError(err: Exception) {
// Clear various auth related fields on failure
when (SshSessionFactory.getInstance()) {
is SshApiSessionFactory -> {
PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext)
.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) }
}
is SshjSessionFactory -> {
callingActivity.applicationContext
.getEncryptedPrefs("git_operation")
.edit {
remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE)
remove(PreferenceKeys.HTTPS_PASSWORD)
}
}
}
}
/**
* Action to execute on success
*/
open fun onSuccess() {}
companion object {
const val GET_SSH_KEY_FROM_CLONE = 201
}
}
|