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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
|
package com.zeapo.pwdstore
import android.Manifest
import android.accessibilityservice.AccessibilityServiceInfo
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.preference.CheckBoxPreference
import android.preference.Preference
import android.preference.PreferenceFragment
import android.preference.PreferenceManager
import android.provider.Settings
import android.support.design.widget.Snackbar
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.MenuItem
import android.view.accessibility.AccessibilityManager
import android.widget.TextView
import android.widget.Toast
import com.nononsenseapps.filepicker.FilePickerActivity
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
import com.zeapo.pwdstore.crypto.PgpActivity
import com.zeapo.pwdstore.git.GitActivity
import com.zeapo.pwdstore.utils.PasswordRepository
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.openintents.openpgp.util.OpenPgpUtils
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
class UserPreference : AppCompatActivity() {
private lateinit var prefsFragment: PrefsFragment
class PrefsFragment : PreferenceFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callingActivity = activity as UserPreference
val sharedPreferences = preferenceManager.sharedPreferences
addPreferencesFromResource(R.xml.preference)
findPreference("app_version").summary = "Version: ${BuildConfig.VERSION_NAME}"
findPreference("openpgp_key_id_pref").onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(callingActivity, PgpActivity::class.java)
intent.putExtra("OPERATION", "GET_KEY_ID")
startActivityForResult(intent, IMPORT_PGP_KEY)
true
}
findPreference("ssh_key").onPreferenceClickListener = Preference.OnPreferenceClickListener {
callingActivity.getSshKeyWithPermissions(sharedPreferences.getBoolean("use_android_file_picker", false))
true
}
findPreference("ssh_keygen").onPreferenceClickListener = Preference.OnPreferenceClickListener {
callingActivity.makeSshKey(true)
true
}
findPreference("ssh_see_key").onPreferenceClickListener = Preference.OnPreferenceClickListener {
val df = SshKeyGen.ShowSshKeyFragment()
df.show(fragmentManager, "public_key")
true
}
findPreference("git_server_info").onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(callingActivity, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.EDIT_SERVER)
startActivityForResult(intent, EDIT_GIT_INFO)
true
}
findPreference("git_config").onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(callingActivity, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.EDIT_GIT_CONFIG)
startActivityForResult(intent, EDIT_GIT_CONFIG)
true
}
findPreference("git_delete_repo").onPreferenceClickListener = Preference.OnPreferenceClickListener {
val repoDir = PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext)
AlertDialog.Builder(callingActivity)
.setTitle(R.string.pref_dialog_delete_title)
.setMessage("${resources.getString(R.string.dialog_delete_msg)} \n $repoDir")
.setCancelable(false)
.setPositiveButton(R.string.dialog_delete) { dialogInterface, _ ->
try {
FileUtils.cleanDirectory(PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext))
PasswordRepository.closeRepository()
} catch (e: Exception) {
//TODO Handle the diffent cases of exceptions
}
sharedPreferences.edit().putBoolean("repository_initialized", false).apply()
dialogInterface.cancel()
callingActivity.finish()
}.setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ -> run { dialogInterface.cancel() } }
.show()
true
}
val externalRepo = findPreference("pref_select_external")
externalRepo.summary = sharedPreferences.getString("git_external_repo", callingActivity.getString(R.string.no_repo_selected))
externalRepo.onPreferenceClickListener = Preference.OnPreferenceClickListener {
callingActivity.selectExternalGitRepository()
true
}
val resetRepo = Preference.OnPreferenceChangeListener { _, o ->
findPreference("git_delete_repo").isEnabled = !(o as Boolean)
PasswordRepository.closeRepository()
sharedPreferences.edit().putBoolean("repo_changed", true).apply()
true
}
findPreference("pref_select_external").onPreferenceChangeListener = resetRepo
findPreference("git_external").onPreferenceChangeListener = resetRepo
findPreference("autofill_apps").onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(callingActivity, AutofillPreferenceActivity::class.java)
startActivity(intent)
true
}
findPreference("autofill_enable").onPreferenceClickListener = Preference.OnPreferenceClickListener {
AlertDialog.Builder(callingActivity).setTitle(R.string.pref_autofill_enable_title).setView(R.layout.autofill_instructions).setPositiveButton(R.string.dialog_ok) { _, _ ->
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
startActivity(intent)
}.setNegativeButton(R.string.dialog_cancel, null).setOnDismissListener { (findPreference("autofill_enable") as CheckBoxPreference).isChecked = (activity as UserPreference).isServiceEnabled }.show()
true
}
findPreference("export_passwords").onPreferenceClickListener = Preference.OnPreferenceClickListener {
callingActivity.exportPasswordsWithPermissions()
true
}
}
override fun onStart() {
super.onStart()
val sharedPreferences = preferenceManager.sharedPreferences
findPreference("pref_select_external").summary = preferenceManager.sharedPreferences.getString("git_external_repo", getString(R.string.no_repo_selected))
findPreference("ssh_see_key").isEnabled = sharedPreferences.getBoolean("use_generated_key", false)
findPreference("git_delete_repo").isEnabled = !sharedPreferences.getBoolean("git_external", false)
val keyPref = findPreference("openpgp_key_id_pref")
val selectedKeys: Array<String> = ArrayList<String>(sharedPreferences.getStringSet("openpgp_key_ids_set", HashSet<String>())).toTypedArray()
if (selectedKeys.isEmpty()) {
keyPref.summary = "No key selected"
} else {
keyPref.summary = selectedKeys.joinToString(separator = ";") {
s ->
OpenPgpUtils.convertKeyIdToHex(java.lang.Long.valueOf(s))
}
}
// see if the autofill service is enabled and check the preference accordingly
(findPreference("autofill_enable") as CheckBoxPreference).isChecked = (activity as UserPreference).isServiceEnabled
}
}
public override fun onCreate(savedInstanceState: Bundle?) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.applicationContext)
super.onCreate(savedInstanceState)
when (intent?.getStringExtra("operation")) {
"get_ssh_key" -> getSshKeyWithPermissions(sharedPreferences.getBoolean("use_android_file_picker", false))
"make_ssh_key" -> makeSshKey(false)
"git_external" -> selectExternalGitRepository()
}
prefsFragment = PrefsFragment()
fragmentManager.beginTransaction().replace(android.R.id.content, prefsFragment).commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
fun selectExternalGitRepository() {
val activity = this
AlertDialog.Builder(this)
.setTitle("Choose where to store the passwords")
.setMessage("You must select a directory where to store your passwords. If you want " +
"to store your passwords within the hidden storage of the application, " +
"cancel this dialog and disable the \"External Repository\" option.")
.setPositiveButton(R.string.dialog_ok) { _, _ ->
// This always works
val i = Intent(activity.applicationContext, FilePickerActivity::class.java)
// This works if you defined the intent filter
// Intent i = new Intent(Intent.ACTION_GET_CONTENT);
// Set these depending on your use case. These are the defaults.
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
i.putExtra(FilePickerActivity.EXTRA_START_PATH, Environment.getExternalStorageDirectory().path)
startActivityForResult(i, SELECT_GIT_DIRECTORY)
}.setNegativeButton(R.string.dialog_cancel, null).show()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
val id = item.itemId
when (id) {
android.R.id.home -> {
setResult(Activity.RESULT_OK)
finish()
return true
}
}
return super.onOptionsItemSelected(item)
}
/**
* Opens a file explorer to import the private key
*/
fun getSshKeyWithPermissions(useDefaultPicker: Boolean) = runWithPermissions(
requestedPermission = Manifest.permission.READ_EXTERNAL_STORAGE,
requestCode = REQUEST_EXTERNAL_STORAGE_SSH_KEY,
reason = "We need access to the sd-card to import the ssh-key"
) {
getSshKey(useDefaultPicker)
}
/**
* Opens a file explorer to import the private key
*/
fun getSshKey(useDefaultPicker: Boolean) {
val intent = if (useDefaultPicker) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.setType("*/*")
} else {
// This always works
val intent = Intent(applicationContext, FilePickerActivity::class.java)
// Set these depending on your use case. These are the defaults.
intent.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
intent.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false)
intent.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE)
intent.putExtra(FilePickerActivity.EXTRA_START_PATH, Environment.getExternalStorageDirectory().path)
}
startActivityForResult(intent, IMPORT_SSH_KEY)
}
/**
* Run a function after checking that the permissions have been requested
*
* @param requestedPermission The permission to request
* @param requestCode The code passed to onRequestPermissionsResult
* @param reason The text to be shown to the user to explain why we're requesting this permission
* @param body The function to run
*/
fun runWithPermissions(requestedPermission: String, requestCode: Int, reason: String, body: () -> Unit): Unit {
if (ContextCompat.checkSelfPermission(this, requestedPermission) != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, requestedPermission)) {
val snack = Snackbar.make(prefsFragment.view,
reason,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.dialog_ok) {
ActivityCompat.requestPermissions(this, arrayOf(requestedPermission), requestCode)
}
snack.show()
val view = snack.view
val tv = view.findViewById(android.support.design.R.id.snackbar_text) as TextView
tv.setTextColor(Color.WHITE)
tv.maxLines = 10
} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(this, arrayOf(requestedPermission), requestCode)
}
} else {
body()
}
}
/**
* Exports the passwords after requesting permissions
*/
fun exportPasswordsWithPermissions() = runWithPermissions(
requestedPermission = Manifest.permission.WRITE_EXTERNAL_STORAGE,
requestCode = REQUEST_EXTERNAL_STORAGE_SSH_KEY,
reason = "We need access to the sd-card to export the passwords"
) {
exportPasswords()
}
/**
* Exports the passwords
*/
fun exportPasswords(): Unit {
val i = Intent(applicationContext, FilePickerActivity::class.java)
// Set these depending on your use case. These are the defaults.
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
i.putExtra(FilePickerActivity.EXTRA_START_PATH, Environment.getExternalStorageDirectory().path)
startActivityForResult(i, EXPORT_PASSWORDS)
}
/**
* Opens a key generator to generate a public/private key pair
*/
fun makeSshKey(fromPreferences: Boolean) {
val intent = Intent(applicationContext, SshKeyGen::class.java)
startActivity(intent)
if (!fromPreferences) {
setResult(Activity.RESULT_OK)
finish()
}
}
@Throws(IOException::class)
private fun copySshKey(uri: Uri) {
val sshKey = this.contentResolver.openInputStream(uri)
if (sshKey != null) {
val privateKey = IOUtils.toByteArray(sshKey)
FileUtils.writeByteArrayToFile(File(filesDir.toString() + "/.ssh_key"), privateKey)
sshKey.close()
} else {
Toast.makeText(this, getString(R.string.ssh_key_does_not_exist), Toast.LENGTH_LONG).show()
}
}
// Returns whether the autofill service is enabled
private val isServiceEnabled: Boolean
get() {
val am = this
.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
val runningServices = am
.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC)
return runningServices.any { "com.zeapo.pwdstore/.autofill.AutofillService" == it.id }
}
override fun onActivityResult(requestCode: Int, resultCode: Int,
data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
if (data == null) {
setResult(Activity.RESULT_CANCELED)
return
}
when (requestCode) {
IMPORT_SSH_KEY -> {
try {
val uri: Uri = data.data ?: throw IOException("Unable to open file")
copySshKey(uri)
Toast.makeText(this, this.resources.getString(R.string.ssh_key_success_dialog_title), Toast.LENGTH_LONG).show()
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
prefs.edit().putBoolean("use_generated_key", false).apply()
//delete the public key from generation
val file = File(filesDir.toString() + "/.ssh_key.pub")
file.delete()
setResult(Activity.RESULT_OK)
finish()
} catch (e: IOException) {
AlertDialog.Builder(this).setTitle(this.resources.getString(R.string.ssh_key_error_dialog_title)).setMessage(this.resources.getString(R.string.ssh_key_error_dialog_text) + e.message).setPositiveButton(this.resources.getString(R.string.dialog_ok)) { _, _ ->
// pass
}.show()
}
}
EDIT_GIT_INFO -> {
}
SELECT_GIT_DIRECTORY -> {
val uri = data.data
if (uri.path == Environment.getExternalStorageDirectory().path) {
// the user wants to use the root of the sdcard as a store...
AlertDialog.Builder(this)
.setTitle("SD-Card root selected")
.setMessage("You have selected the root of your sdcard for the store. " +
"This is extremely dangerous and you will lose your data " +
"as its content will, eventually, be deleted")
.setPositiveButton("Remove everything") { _, _ ->
PreferenceManager.getDefaultSharedPreferences(applicationContext)
.edit()
.putString("git_external_repo", uri.path)
.apply()
}.setNegativeButton(R.string.dialog_cancel, null).show()
} else {
PreferenceManager.getDefaultSharedPreferences(applicationContext)
.edit()
.putString("git_external_repo", uri.path)
.apply()
}
}
EXPORT_PASSWORDS -> {
val uri = data.data
val repositoryDirectory = PasswordRepository.getRepositoryDirectory(applicationContext)
val fmtOut = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.US)
val date = Date()
val password_now = "/password_store_" + fmtOut.format(date)
val targetDirectory = File(uri.path + password_now)
if (repositoryDirectory != null) {
try {
FileUtils.copyDirectory(repositoryDirectory, targetDirectory, true)
} catch (e: IOException) {
Log.d("PWD_EXPORT", "Exception happened : " + e.message)
}
}
}
else -> {
}
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.applicationContext)
when (requestCode) {
REQUEST_EXTERNAL_STORAGE_SSH_KEY -> {
// If request is cancelled, the result arrays are empty.
if (grantResults.isNotEmpty() && PackageManager.PERMISSION_GRANTED in grantResults) {
getSshKey(sharedPreferences.getBoolean("use_android_file_picker", false))
}
}
REQUEST_EXTERNAL_STORAGE_EXPORT_PWD -> {
if (grantResults.isNotEmpty() && PackageManager.PERMISSION_GRANTED in grantResults) {
exportPasswords()
}
}
}
}
companion object {
private val IMPORT_SSH_KEY = 1
private val IMPORT_PGP_KEY = 2
private val EDIT_GIT_INFO = 3
private val SELECT_GIT_DIRECTORY = 4
private val EXPORT_PASSWORDS = 5
private val EDIT_GIT_CONFIG = 6
private val REQUEST_EXTERNAL_STORAGE_SSH_KEY = 50
private val REQUEST_EXTERNAL_STORAGE_EXPORT_PWD = 51
}
}
|