aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/com
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/com')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt30
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt11
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt13
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt62
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt19
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/UserPreference.kt69
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt31
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt19
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt270
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt202
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt89
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt296
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt788
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitAsyncTask.kt7
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt6
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/ClipboardUtils.kt28
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt49
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt4
21 files changed, 1019 insertions, 980 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt b/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt
index 5349d6c3..ceb84020 100644
--- a/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt
@@ -8,17 +8,16 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
-import android.content.ClipboardManager
+import android.content.ClipData
import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.d
-import com.zeapo.pwdstore.utils.ClipboardUtils
+import com.zeapo.pwdstore.utils.clipboard
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -60,7 +59,6 @@ class ClipboardService : Service() {
startTimer(time)
}
withContext(Dispatchers.Main) {
- emitBroadcast()
clearClipboard()
stopForeground(true)
stopSelf()
@@ -85,11 +83,21 @@ class ClipboardService : Service() {
private fun clearClipboard() {
val deepClear = settings.getBoolean("clear_clipboard_20x", false)
- val clipboardManager = getSystemService<ClipboardManager>()
+ val clipboard = clipboard
- if (clipboardManager is ClipboardManager) {
+ if (clipboard != null) {
scope.launch {
- ClipboardUtils.clearClipboard(clipboardManager, deepClear)
+ d { "Clearing the clipboard" }
+ val clip = ClipData.newPlainText("pgp_handler_result_pm", "")
+ clipboard.setPrimaryClip(clip)
+ if (deepClear) {
+ withContext(Dispatchers.IO) {
+ repeat(20) {
+ val count = (it * 500).toString()
+ clipboard.setPrimaryClip(ClipData.newPlainText(count, count))
+ }
+ }
+ }
}
} else {
d { "Cannot get clipboard manager service" }
@@ -105,12 +113,6 @@ class ClipboardService : Service() {
}
}
- private fun emitBroadcast() {
- val localBroadcastManager = LocalBroadcastManager.getInstance(this)
- val clearIntent = Intent(ACTION_CLEAR)
- localBroadcastManager.sendBroadcast(clearIntent)
- }
-
private fun createNotification() {
createNotificationChannel()
val clearIntent = Intent(this, ClipboardService::class.java)
@@ -151,7 +153,7 @@ class ClipboardService : Service() {
companion object {
private const val ACTION_CLEAR = "ACTION_CLEAR_CLIPBOARD"
- private const val ACTION_START = "ACTION_START_CLIPBOARD_TIMER"
+ const val ACTION_START = "ACTION_START_CLIPBOARD_TIMER"
private const val CHANNEL_ID = "NotificationService"
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt b/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt
index b5902199..b452f521 100644
--- a/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt
@@ -10,7 +10,7 @@ import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.preference.PreferenceManager
-import com.zeapo.pwdstore.crypto.PgpActivity
+import com.zeapo.pwdstore.crypto.DecryptActivity
import com.zeapo.pwdstore.utils.BiometricAuthenticator
class LaunchActivity : AppCompatActivity() {
@@ -39,13 +39,12 @@ class LaunchActivity : AppCompatActivity() {
}
private fun startTargetActivity(noAuth: Boolean) {
- if (intent?.getStringExtra("OPERATION") == "DECRYPT") {
- val decryptIntent = Intent(this, PgpActivity::class.java)
+ if (intent.action == ACTION_DECRYPT_PASS) {
+ val decryptIntent = Intent(this, DecryptActivity::class.java)
decryptIntent.putExtra("NAME", intent.getStringExtra("NAME"))
decryptIntent.putExtra("FILE_PATH", intent.getStringExtra("FILE_PATH"))
decryptIntent.putExtra("REPO_PATH", intent.getStringExtra("REPO_PATH"))
decryptIntent.putExtra("LAST_CHANGED_TIMESTAMP", intent.getLongExtra("LAST_CHANGED_TIMESTAMP", 0L))
- decryptIntent.putExtra("OPERATION", "DECRYPT")
startActivity(decryptIntent)
} else {
startActivity(Intent(this, PasswordStore::class.java))
@@ -53,4 +52,8 @@ class LaunchActivity : AppCompatActivity() {
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
Handler().postDelayed({ finish() }, if (noAuth) 0L else 500L)
}
+
+ companion object {
+ const val ACTION_DECRYPT_PASS = "DECRYPT_PASS"
+ }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
index 03f2cfe1..24a1b7cb 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
@@ -155,11 +155,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
// Called each time the action mode is shown. Always called after onCreateActionMode, but
// may be called multiple times if the mode is invalidated.
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
- menu.findItem(R.id.menu_edit_password).isVisible =
- recyclerAdapter.getSelectedItems(requireContext())
- .map { it.type == PasswordItem.TYPE_PASSWORD }
- .singleOrNull() == true
- return true // Return false if nothing is done
+ return true
}
// Called when the user selects a contextual menu item
@@ -174,13 +170,6 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
mode.finish() // Action picked, so close the CAB
return true
}
- R.id.menu_edit_password -> {
- requireStore().editPassword(
- recyclerAdapter.getSelectedItems(requireContext()).first()
- )
- mode.finish()
- return true
- }
R.id.menu_move_password -> {
requireStore().movePasswords(recyclerAdapter.getSelectedItems(requireContext()))
return false
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
index 00cfbf9a..ab9e3944 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
@@ -46,11 +46,10 @@ import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel
import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel
-import com.zeapo.pwdstore.crypto.PgpActivity
-import com.zeapo.pwdstore.crypto.PgpActivity.Companion.getLongName
+import com.zeapo.pwdstore.crypto.BasePgpActivity.Companion.getLongName
+import com.zeapo.pwdstore.crypto.DecryptActivity
+import com.zeapo.pwdstore.crypto.PasswordCreationActivity
import com.zeapo.pwdstore.git.BaseGitActivity
-import com.zeapo.pwdstore.git.GitAsyncTask
-import com.zeapo.pwdstore.git.GitOperation
import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.git.config.ConnectionMode
@@ -65,6 +64,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirect
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.initialize
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.isInitialized
import com.zeapo.pwdstore.utils.PasswordRepository.PasswordSortOrder.Companion.getSortOrder
+import com.zeapo.pwdstore.utils.commitChange
import com.zeapo.pwdstore.utils.listFilesRecursively
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.errors.GitAPIException
@@ -73,7 +73,7 @@ import java.io.File
import java.lang.Character.UnicodeBlock
import java.util.Stack
-class PasswordStore : AppCompatActivity() {
+class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
private lateinit var activity: PasswordStore
private lateinit var searchItem: MenuItem
@@ -123,7 +123,6 @@ class PasswordStore : AppCompatActivity() {
savedInstance = null
}
super.onCreate(savedInstance)
- setContentView(R.layout.activity_pwdstore)
// If user is eligible for Oreo autofill, prompt them to switch.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
@@ -487,15 +486,16 @@ class PasswordStore : AppCompatActivity() {
}
fun decryptPassword(item: PasswordItem) {
- val decryptIntent = Intent(this, PgpActivity::class.java)
+ val decryptIntent = Intent(this, DecryptActivity::class.java)
val authDecryptIntent = Intent(this, LaunchActivity::class.java)
for (intent in arrayOf(decryptIntent, authDecryptIntent)) {
intent.putExtra("NAME", item.toString())
intent.putExtra("FILE_PATH", item.file.absolutePath)
intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath)
intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.file.absolutePath))
- intent.putExtra("OPERATION", "DECRYPT")
}
+ // Needs an action to be a shortcut intent
+ authDecryptIntent.action = LaunchActivity.ACTION_DECRYPT_PASS
// Adds shortcut
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
@@ -503,7 +503,7 @@ class PasswordStore : AppCompatActivity() {
.setShortLabel(item.toString())
.setLongLabel(item.fullPathToParent + item.toString())
.setIcon(Icon.createWithResource(this, R.mipmap.ic_launcher))
- .setIntent(authDecryptIntent.setAction("DECRYPT_PASS")) // Needs action
+ .setIntent(authDecryptIntent)
.build()
val shortcuts = shortcutManager!!.dynamicShortcuts
if (shortcuts.size >= shortcutManager!!.maxShortcutCountPerActivity && shortcuts.size > 0) {
@@ -517,16 +517,6 @@ class PasswordStore : AppCompatActivity() {
startActivityForResult(decryptIntent, REQUEST_CODE_DECRYPT_AND_VERIFY)
}
- fun editPassword(item: PasswordItem) {
- val intent = Intent(this, PgpActivity::class.java)
- intent.putExtra("NAME", item.toString())
- intent.putExtra("FILE_PATH", item.file.absolutePath)
- intent.putExtra("PARENT_PATH", item.file.parentFile!!.absolutePath)
- intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath)
- intent.putExtra("OPERATION", "EDIT")
- startActivityForResult(intent, REQUEST_CODE_EDIT)
- }
-
private fun validateState(): Boolean {
if (!isInitialized) {
MaterialAlertDialogBuilder(this)
@@ -553,10 +543,9 @@ class PasswordStore : AppCompatActivity() {
if (!validateState()) return
val currentDir = currentDir
tag(TAG).i { "Adding file to : ${currentDir.absolutePath}" }
- val intent = Intent(this, PgpActivity::class.java)
+ val intent = Intent(this, PasswordCreationActivity::class.java)
intent.putExtra("FILE_PATH", currentDir.absolutePath)
intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath)
- intent.putExtra("OPERATION", "ENCRYPT")
startActivityForResult(intent, REQUEST_CODE_ENCRYPT)
}
@@ -626,10 +615,6 @@ class PasswordStore : AppCompatActivity() {
private val currentDir: File
get() = plist?.currentDir ?: getRepositoryDirectory(applicationContext)
- private fun commitChange(message: String) {
- commitChange(this, message)
- }
-
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
@@ -650,11 +635,6 @@ class PasswordStore : AppCompatActivity() {
data!!.extras!!.getString("LONG_NAME")))
refreshPasswordList()
}
- REQUEST_CODE_EDIT -> {
- commitChange(resources.getString(R.string.git_commit_edit_text,
- data!!.extras!!.getString("LONG_NAME")))
- refreshPasswordList()
- }
BaseGitActivity.REQUEST_INIT, NEW_REPO_BUTTON -> initializeRepositoryInfo()
BaseGitActivity.REQUEST_SYNC, BaseGitActivity.REQUEST_PULL -> resetPasswordList()
HOME -> checkLocalRepository()
@@ -821,7 +801,6 @@ class PasswordStore : AppCompatActivity() {
companion object {
const val REQUEST_CODE_ENCRYPT = 9911
const val REQUEST_CODE_DECRYPT_AND_VERIFY = 9913
- const val REQUEST_CODE_EDIT = 9916
const val REQUEST_CODE_SELECT_FOLDER = 9917
const val REQUEST_ARG_PATH = "PATH"
private val TAG = PasswordStore::class.java.name
@@ -836,26 +815,5 @@ class PasswordStore : AppCompatActivity() {
}
private const val PREFERENCE_SEEN_AUTOFILL_ONBOARDING = "seen_autofill_onboarding"
-
- fun commitChange(activity: Activity, message: String, finishWithResultOnEnd: Intent? = null) {
- if (!PasswordRepository.isGitRepo()) {
- if (finishWithResultOnEnd != null) {
- activity.setResult(Activity.RESULT_OK, finishWithResultOnEnd)
- activity.finish()
- }
- return
- }
- object : GitOperation(getRepositoryDirectory(activity), activity) {
- override fun execute() {
- tag(TAG).d { "Committing with message $message" }
- val git = Git(repository)
- val tasks = GitAsyncTask(activity, true, this, finishWithResultOnEnd)
- tasks.execute(
- git.add().addFilepattern("."),
- git.commit().setAll(true).setMessage(message)
- )
- }
- }.execute()
- }
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt b/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt
index 4d8475df..568f86f3 100644
--- a/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt
@@ -10,21 +10,16 @@ import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.commit
import com.zeapo.pwdstore.utils.PasswordRepository
-// TODO more work needed, this is just an extraction from PgpHandler
-class SelectFolderActivity : AppCompatActivity() {
+class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
private lateinit var passwordList: SelectFolderFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.select_folder_layout)
-
- val fragmentManager = supportFragmentManager
- val fragmentTransaction = fragmentManager.beginTransaction()
-
passwordList = SelectFolderFragment()
val args = Bundle()
args.putString(PasswordStore.REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory(applicationContext).absolutePath)
@@ -33,10 +28,11 @@ class SelectFolderActivity : AppCompatActivity() {
supportActionBar?.show()
- fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+ supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
- fragmentTransaction.replace(R.id.pgp_handler_linearlayout, passwordList, "PasswordsList")
- fragmentTransaction.commit()
+ supportFragmentManager.commit {
+ replace(R.id.pgp_handler_linearlayout, passwordList, "PasswordsList")
+ }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -45,8 +41,7 @@ class SelectFolderActivity : AppCompatActivity() {
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
- val id = item.itemId
- when (id) {
+ when (item.itemId) {
android.R.id.home -> {
setResult(Activity.RESULT_CANCELED)
finish()
diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
index 96f23724..04967c82 100644
--- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
@@ -21,6 +21,7 @@ import android.text.TextUtils
import android.view.MenuItem
import android.view.accessibility.AccessibilityManager
import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatTextView
import androidx.biometric.BiometricManager
@@ -42,7 +43,8 @@ import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel
import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel
-import com.zeapo.pwdstore.crypto.PgpActivity
+import com.zeapo.pwdstore.crypto.BasePgpActivity
+import com.zeapo.pwdstore.crypto.GetKeyIdsActivity
import com.zeapo.pwdstore.git.GitConfigActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.pwgenxkpwd.XkpwdDictionary
@@ -74,12 +76,13 @@ class UserPreference : AppCompatActivity() {
private lateinit var autofillDependencies: List<Preference>
private lateinit var oreoAutofillDependencies: List<Preference>
private lateinit var callingActivity: UserPreference
+ private lateinit var sharedPreferences: SharedPreferences
private lateinit var encryptedPreferences: SharedPreferences
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
callingActivity = requireActivity() as UserPreference
val context = requireContext()
- val sharedPreferences = preferenceManager.sharedPreferences
+ sharedPreferences = preferenceManager.sharedPreferences
encryptedPreferences = requireActivity().applicationContext.getEncryptedPrefs("git_operation")
addPreferencesFromResource(R.xml.preference)
@@ -146,15 +149,6 @@ class UserPreference : AppCompatActivity() {
viewSshKeyPreference?.isVisible = sharedPreferences.getBoolean("use_generated_key", false)
deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean("git_external", false)
clearClipboard20xPreference?.isVisible = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
- val selectedKeys = (sharedPreferences.getStringSet("openpgp_key_ids_set", null)
- ?: HashSet()).toTypedArray()
- keyPreference?.summary = if (selectedKeys.isEmpty()) {
- this.resources.getString(R.string.pref_no_key_selected)
- } else {
- selectedKeys.joinToString(separator = ";") { s ->
- OpenPgpUtils.convertKeyIdToHex(java.lang.Long.valueOf(s))
- }
- }
openkeystoreIdPreference?.isVisible = sharedPreferences.getString("ssh_openkeystore_keyid", null)?.isNotEmpty()
?: false
@@ -163,16 +157,21 @@ class UserPreference : AppCompatActivity() {
appVersionPreference?.summary = "Version: ${BuildConfig.VERSION_NAME}"
- keyPreference?.onPreferenceClickListener = ClickListener {
- val providerPackageName = requireNotNull(sharedPreferences.getString("openpgp_provider_list", ""))
- if (providerPackageName.isEmpty()) {
- Snackbar.make(requireView(), resources.getString(R.string.provider_toast_text), Snackbar.LENGTH_LONG).show()
- false
- } else {
- val intent = Intent(callingActivity, PgpActivity::class.java)
- intent.putExtra("OPERATION", "GET_KEY_ID")
- startActivityForResult(intent, IMPORT_PGP_KEY)
- true
+ keyPreference?.let { pref ->
+ updateKeyIDsSummary(pref)
+ pref.onPreferenceClickListener = ClickListener {
+ val providerPackageName = requireNotNull(sharedPreferences.getString("openpgp_provider_list", ""))
+ if (providerPackageName.isEmpty()) {
+ Snackbar.make(requireView(), resources.getString(R.string.provider_toast_text), Snackbar.LENGTH_LONG).show()
+ false
+ } else {
+ val intent = Intent(callingActivity, GetKeyIdsActivity::class.java)
+ val keySelectResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ updateKeyIDsSummary(pref)
+ }
+ keySelectResult.launch(intent)
+ true
+ }
}
}
@@ -366,13 +365,25 @@ class UserPreference : AppCompatActivity() {
}
}
+ private fun updateKeyIDsSummary(preference: Preference) {
+ val selectedKeys = (sharedPreferences.getStringSet("openpgp_key_ids_set", null)
+ ?: HashSet()).toTypedArray()
+ preference.summary = if (selectedKeys.isEmpty()) {
+ resources.getString(R.string.pref_no_key_selected)
+ } else {
+ selectedKeys.joinToString(separator = ";") { s ->
+ OpenPgpUtils.convertKeyIdToHex(s.toLong())
+ }
+ }
+ }
+
private fun updateXkPasswdPrefsVisibility(newValue: Any?, prefIsCustomDict: CheckBoxPreference?, prefCustomDictPicker: Preference?) {
when (newValue as String) {
- PgpActivity.KEY_PWGEN_TYPE_CLASSIC -> {
+ BasePgpActivity.KEY_PWGEN_TYPE_CLASSIC -> {
prefIsCustomDict?.isVisible = false
prefCustomDictPicker?.isVisible = false
}
- PgpActivity.KEY_PWGEN_TYPE_XKPASSWD -> {
+ BasePgpActivity.KEY_PWGEN_TYPE_XKPASSWD -> {
prefIsCustomDict?.isVisible = true
prefCustomDictPicker?.isVisible = true
}
@@ -653,8 +664,6 @@ class UserPreference : AppCompatActivity() {
.show()
}
}
- EDIT_GIT_INFO -> {
- }
SELECT_GIT_DIRECTORY -> {
val uri = data.data
@@ -792,12 +801,10 @@ class UserPreference : AppCompatActivity() {
companion object {
private const val IMPORT_SSH_KEY = 1
- private const val IMPORT_PGP_KEY = 2
- private const val EDIT_GIT_INFO = 3
- private const val SELECT_GIT_DIRECTORY = 4
- private const val EXPORT_PASSWORDS = 5
- private const val EDIT_GIT_CONFIG = 6
- private const val SET_CUSTOM_XKPWD_DICT = 7
+ private const val SELECT_GIT_DIRECTORY = 2
+ private const val EXPORT_PASSWORDS = 3
+ private const val EDIT_GIT_CONFIG = 4
+ private const val SET_CUSTOM_XKPWD_DICT = 5
private const val TAG = "UserPreference"
/**
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt
index 3553d431..860d8459 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt
@@ -16,31 +16,32 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.databinding.AutofillRecyclerViewBinding
+import com.zeapo.pwdstore.utils.viewBinding
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.lang.ref.WeakReference
import java.util.ArrayList
class AutofillPreferenceActivity : AppCompatActivity() {
+ private val binding by viewBinding(AutofillRecyclerViewBinding::inflate)
internal var recyclerAdapter: AutofillRecyclerAdapter? = null // let fragment have access
- private var recyclerView: RecyclerView? = null
private var pm: PackageManager? = null
private var recreate: Boolean = false // flag for action on up press; origin autofill dialog? different act
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
- setContentView(R.layout.autofill_recycler_view)
- recyclerView = findViewById(R.id.autofill_recycler)
+ setContentView(binding.root)
val layoutManager = LinearLayoutManager(this)
- recyclerView!!.layoutManager = layoutManager
- recyclerView!!.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
- FastScrollerBuilder(recyclerView!!).build()
+ with(binding) {
+ autofillRecycler.layoutManager = layoutManager
+ autofillRecycler.addItemDecoration(DividerItemDecoration(this@AutofillPreferenceActivity, DividerItemDecoration.VERTICAL))
+ FastScrollerBuilder(autofillRecycler).build()
+ }
pm = packageManager
@@ -105,7 +106,7 @@ class AutofillPreferenceActivity : AppCompatActivity() {
companion object {
private class PopulateTask(activity: AutofillPreferenceActivity) : AsyncTask<Void, Void, Void>() {
- val weakReference = WeakReference<AutofillPreferenceActivity>(activity)
+ val weakReference = WeakReference(activity)
override fun onPreExecute() {
weakReference.get()?.apply {
@@ -140,11 +141,13 @@ class AutofillPreferenceActivity : AppCompatActivity() {
override fun onPostExecute(ignored: Void?) {
weakReference.get()?.apply {
runOnUiThread {
- findViewById<View>(R.id.progress_bar).visibility = View.GONE
- recyclerView!!.adapter = recyclerAdapter
- val extras = intent.extras
- if (extras != null) {
- recyclerView!!.scrollToPosition(recyclerAdapter!!.getPosition(extras.getString("appName")!!))
+ with(binding) {
+ progressBar.visibility = View.GONE
+ autofillRecycler.adapter = recyclerAdapter
+ val extras = intent.extras
+ if (extras != null) {
+ autofillRecycler.scrollToPosition(recyclerAdapter!!.getPosition(extras.getString("appName")!!))
+ }
}
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt
index 209892a7..fad13ec8 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt
@@ -23,8 +23,9 @@ import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
import com.zeapo.pwdstore.autofill.oreo.Credentials
import com.zeapo.pwdstore.autofill.oreo.FillableForm
import com.zeapo.pwdstore.autofill.oreo.FormOrigin
-import com.zeapo.pwdstore.crypto.PgpActivity
+import com.zeapo.pwdstore.crypto.PasswordCreationActivity
import com.zeapo.pwdstore.utils.PasswordRepository
+import com.zeapo.pwdstore.utils.commitChange
import java.io.File
@RequiresApi(Build.VERSION_CODES.O)
@@ -97,15 +98,14 @@ class AutofillSaveActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val repo = PasswordRepository.getRepositoryDirectory(applicationContext)
- val saveIntent = Intent(this, PgpActivity::class.java).apply {
+ val saveIntent = Intent(this, PasswordCreationActivity::class.java).apply {
putExtras(
bundleOf(
"REPO_PATH" to repo.absolutePath,
"FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath,
- "OPERATION" to "ENCRYPT",
- "SUGGESTED_NAME" to intent.getStringExtra(EXTRA_NAME),
- "SUGGESTED_PASS" to intent.getStringExtra(EXTRA_PASSWORD),
- "GENERATE_PASSWORD" to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
+ PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
+ PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
+ PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
)
)
}
@@ -144,10 +144,9 @@ class AutofillSaveActivity : Activity() {
// Password was extracted from a form, there is nothing to fill.
Intent()
}
- // PgpActivity delegates committing the added file to PasswordStore. Since PasswordStore
- // is not involved in an AutofillScenario, we have to commit the file ourselves.
- PasswordStore.commitChange(
- this,
+ // PasswordCreationActivity delegates committing the added file to PasswordStore. Since
+ // PasswordStore is not involved in an AutofillScenario, we have to commit the file ourselves.
+ commitChange(
getString(R.string.git_commit_add_text, longName),
finishWithResultOnEnd = result
)
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
new file mode 100644
index 00000000..5206a15f
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package com.zeapo.pwdstore.crypto
+
+import android.app.PendingIntent
+import android.content.ClipData
+import android.content.Intent
+import android.content.IntentSender
+import android.content.SharedPreferences
+import android.os.Build
+import android.os.Bundle
+import android.text.format.DateUtils
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.annotation.CallSuper
+import androidx.appcompat.app.AppCompatActivity
+import androidx.preference.PreferenceManager
+import com.github.ajalt.timberkt.Timber.tag
+import com.github.ajalt.timberkt.e
+import com.github.ajalt.timberkt.i
+import com.google.android.material.snackbar.Snackbar
+import com.zeapo.pwdstore.ClipboardService
+import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.UserPreference
+import com.zeapo.pwdstore.utils.clipboard
+import com.zeapo.pwdstore.utils.snackbar
+import me.msfjarvis.openpgpktx.util.OpenPgpApi
+import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
+import org.openintents.openpgp.IOpenPgpService2
+import org.openintents.openpgp.OpenPgpError
+import java.io.File
+
+@Suppress("Registered")
+open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
+
+ /**
+ * Full path to the repository
+ */
+ val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") }
+
+ /**
+ * Full path to the password file being worked on
+ */
+ val fullPath: String by lazy { intent.getStringExtra("FILE_PATH") }
+
+ /**
+ * Name of the password file
+ *
+ * Converts personal/auth.foo.org/john_doe@example.org.gpg to john_doe.example.org
+ */
+ val name: String by lazy { File(fullPath).nameWithoutExtension }
+
+ /**
+ * Get the timestamp for when this file was last modified.
+ */
+ val lastChangedString: CharSequence by lazy {
+ getLastChangedString(
+ intent.getLongExtra(
+ "LAST_CHANGED_TIMESTAMP",
+ -1L
+ )
+ )
+ }
+
+ /**
+ * [SharedPreferences] instance used by subclasses to persist settings
+ */
+ val settings: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
+
+ /**
+ * Read-only field for getting the list of OpenPGP key IDs that we have access to.
+ */
+ var keyIDs = emptySet<String>()
+ private set
+
+ /**
+ * Handle to the [OpenPgpApi] instance that is used by subclasses to interface with OpenKeychain.
+ */
+ private var serviceConnection: OpenPgpServiceConnection? = null
+ var api: OpenPgpApi? = null
+
+ /**
+ * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots
+ * or recent apps screen and fills in [keyIDs] from [settings]
+ */
+ @CallSuper
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
+ tag(TAG)
+
+ keyIDs = settings.getStringSet("openpgp_key_ids_set", null) ?: emptySet()
+ }
+
+ /**
+ * [onDestroy] handles unbinding from the OpenPgp service linked with [serviceConnection]. This
+ * is annotated with [CallSuper] because it's critical to unbind the service to ensure we're not
+ * leaking things.
+ */
+ @CallSuper
+ override fun onDestroy() {
+ super.onDestroy()
+ serviceConnection?.unbindFromService()
+ }
+
+ /**
+ * Sets up [api] once the service is bound. Downstream consumers must call super this to
+ * initialize [api]
+ */
+ @CallSuper
+ override fun onBound(service: IOpenPgpService2) {
+ api = OpenPgpApi(this, service)
+ }
+
+ /**
+ * Mandatory error handling from [OpenPgpServiceConnection.OnBound]. All subclasses must handle
+ * their own errors, and hence this class simply logs and rethrows. Subclasses Must NOT call super.
+ */
+ override fun onError(e: Exception) {
+ e(e) { "Callers must handle their own exceptions" }
+ throw e
+ }
+
+ /**
+ * Method for subclasses to initiate binding with [OpenPgpServiceConnection]. The design choices
+ * here are a bit dubious at first glance. We require passing a [ActivityResultLauncher] because
+ * it lets us react to having a OpenPgp provider selected without relying on the now deprecated
+ * [startActivityForResult].
+ */
+ fun bindToOpenKeychain(onBoundListener: OpenPgpServiceConnection.OnBound, activityResult: ActivityResultLauncher<Intent>) {
+ val providerPackageName = settings.getString("openpgp_provider_list", "")
+ if (providerPackageName.isNullOrEmpty()) {
+ Toast.makeText(this, resources.getString(R.string.provider_toast_text), Toast.LENGTH_LONG).show()
+ activityResult.launch(Intent(this, UserPreference::class.java))
+ } else {
+ serviceConnection = OpenPgpServiceConnection(this, providerPackageName, onBoundListener)
+ serviceConnection?.bindToService()
+ }
+ }
+
+ /**
+ * Handle the case where OpenKeychain returns that it needs to interact with the user
+ *
+ * @param result The intent returned by OpenKeychain
+ */
+ fun getUserInteractionRequestIntent(result: Intent): IntentSender {
+ i { "RESULT_CODE_USER_INTERACTION_REQUIRED" }
+ return (result.getParcelableExtra(OpenPgpApi.RESULT_INTENT) as PendingIntent).intentSender
+ }
+
+ /**
+ * Gets a relative string describing when this shape was last changed
+ * (e.g. "one hour ago")
+ */
+ private fun getLastChangedString(timeStamp: Long): CharSequence {
+ if (timeStamp < 0) {
+ throw RuntimeException()
+ }
+
+ return DateUtils.getRelativeTimeSpanString(this, timeStamp, true)
+ }
+ /**
+ * Base handling of OpenKeychain errors based on the error contained in [result]. Subclasses
+ * can use this when they want to default to sane error handling.
+ */
+ fun handleError(result: Intent) {
+ val error: OpenPgpError? = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)
+ if (error != null) {
+ when (error.errorId) {
+ OpenPgpError.NO_OR_WRONG_PASSPHRASE -> {
+ snackbar(message = getString(R.string.openpgp_error_wrong_passphrase))
+ }
+ OpenPgpError.NO_USER_IDS -> {
+ snackbar(message = getString(R.string.openpgp_error_no_user_ids))
+ }
+ else -> {
+ snackbar(message = getString(R.string.openpgp_error_unknown, error.message))
+ e { "onError getErrorId: ${error.errorId}" }
+ e { "onError getMessage: ${error.message}" }
+ }
+ }
+ }
+ }
+
+ /**
+ * Copies provided [text] to the clipboard. Shows a [Snackbar] which can be disabled by passing
+ * [showSnackbar] as false.
+ */
+ fun copyTextToClipboard(text: String?, showSnackbar: Boolean = true) {
+ val clipboard = clipboard ?: return
+ val clip = ClipData.newPlainText("pgp_handler_result_pm", text)
+ clipboard.setPrimaryClip(clip)
+ if (showSnackbar) {
+ snackbar(message = resources.getString(R.string.clipboard_copied_text))
+ }
+ }
+
+ /**
+ * Copies a provided [password] string to the clipboard. This wraps [copyTextToClipboard] to
+ * hide the default [Snackbar] and starts off an instance of [ClipboardService] to provide a
+ * way of clearing the clipboard.
+ */
+ fun copyPasswordToClipboard(password: String?) {
+ copyTextToClipboard(password, showSnackbar = false)
+
+ var clearAfter = 45
+ try {
+ clearAfter = (settings.getString("general_show_time", "45") ?: "45").toInt()
+ } catch (_: NumberFormatException) {
+ }
+
+ if (clearAfter != 0) {
+ val service = Intent(this, ClipboardService::class.java).apply {
+ action = ClipboardService.ACTION_START
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(service)
+ } else {
+ startService(service)
+ }
+ snackbar(message = resources.getString(R.string.clipboard_password_toast_text, clearAfter))
+ } else {
+ snackbar(message = resources.getString(R.string.clipboard_password_no_clear_toast_text))
+ }
+ }
+
+ companion object {
+ private const val TAG = "APS/BasePgpActivity"
+ const val KEY_PWGEN_TYPE_CLASSIC = "classic"
+ const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd"
+
+ /**
+ * Gets the relative path to the repository
+ */
+ fun getRelativePath(fullPath: String, repositoryPath: String): String =
+ fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/")
+
+ /**
+ * Gets the Parent path, relative to the repository
+ */
+ fun getParentPath(fullPath: String, repositoryPath: String): String {
+ val relativePath = getRelativePath(fullPath, repositoryPath)
+ val index = relativePath.lastIndexOf("/")
+ return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace("/+".toRegex(), "/")
+ }
+
+ /**
+ * /path/to/store/social/facebook.gpg -> social/facebook
+ */
+ @JvmStatic
+ fun getLongName(fullPath: String, repositoryPath: String, basename: String): String {
+ var relativePath = getRelativePath(fullPath, repositoryPath)
+ return if (relativePath.isNotEmpty() && relativePath != "/") {
+ // remove preceding '/'
+ relativePath = relativePath.substring(1)
+ if (relativePath.endsWith('/')) {
+ relativePath + basename
+ } else {
+ "$relativePath/$basename"
+ }
+ } else {
+ basename
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt
new file mode 100644
index 00000000..b7d7adcd
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package com.zeapo.pwdstore.crypto
+
+import android.content.Intent
+import android.graphics.Typeface
+import android.os.Bundle
+import android.text.method.PasswordTransformationMethod
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
+import androidx.lifecycle.lifecycleScope
+import com.github.ajalt.timberkt.e
+import com.zeapo.pwdstore.PasswordEntry
+import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.databinding.DecryptLayoutBinding
+import com.zeapo.pwdstore.utils.viewBinding
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import me.msfjarvis.openpgpktx.util.OpenPgpApi
+import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
+import org.openintents.openpgp.IOpenPgpService2
+import java.io.ByteArrayOutputStream
+import java.io.File
+
+class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
+ private val binding by viewBinding(DecryptLayoutBinding::inflate)
+
+ private val relativeParentPath by lazy { getParentPath(fullPath, repoPath) }
+ private var passwordEntry: PasswordEntry? = null
+
+ private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result ->
+ if (result.data == null) {
+ setResult(RESULT_CANCELED, null)
+ finish()
+ return@registerForActivityResult
+ }
+
+ when (result.resultCode) {
+ RESULT_OK -> decryptAndVerify(result.data)
+ RESULT_CANCELED -> {
+ setResult(RESULT_CANCELED, result.data)
+ finish()
+ }
+ }
+ }
+
+ private val openKeychainResult = registerForActivityResult(StartActivityForResult()) {
+ decryptAndVerify()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ bindToOpenKeychain(this, openKeychainResult)
+ title = name
+ with(binding) {
+ setContentView(root)
+ passwordCategory.text = relativeParentPath
+ passwordFile.text = name
+ passwordFile.setOnLongClickListener {
+ copyTextToClipboard(name)
+ true
+ }
+ try {
+ passwordLastChanged.text = resources.getString(R.string.last_changed, lastChangedString)
+ } catch (e: RuntimeException) {
+ passwordLastChanged.visibility = View.GONE
+ }
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.pgp_handler, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home -> onBackPressed()
+ R.id.edit_password -> editPassword()
+ R.id.share_password_as_plaintext -> shareAsPlaintext()
+ R.id.copy_password -> copyPasswordToClipboard(passwordEntry?.password)
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ override fun onBound(service: IOpenPgpService2) {
+ super.onBound(service)
+ decryptAndVerify()
+ }
+
+ override fun onError(e: Exception) {
+ e(e)
+ }
+
+ /**
+ * Edit the current password and hide all the fields populated by encrypted data so that when
+ * the result triggers they can be repopulated with new data.
+ */
+ private fun editPassword() {
+ val intent = Intent(this, PasswordCreationActivity::class.java)
+ intent.putExtra("FILE_PATH", relativeParentPath)
+ intent.putExtra("REPO_PATH", repoPath)
+ intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name)
+ intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password)
+ intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContent)
+ intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true)
+ startActivity(intent)
+ finish()
+ }
+
+ private fun shareAsPlaintext() {
+ val sendIntent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_TEXT, passwordEntry?.password)
+ type = "text/plain"
+ }
+ // Always show a picker to give the user a chance to cancel
+ startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to)))
+ }
+
+ private fun decryptAndVerify(receivedIntent: Intent? = null) {
+ if (api == null) {
+ bindToOpenKeychain(this, openKeychainResult)
+ return
+ }
+ val data = receivedIntent ?: Intent()
+ data.action = OpenPgpApi.ACTION_DECRYPT_VERIFY
+
+ val inputStream = File(fullPath).inputStream()
+ val outputStream = ByteArrayOutputStream()
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ api?.executeApiAsync(data, inputStream, outputStream) { result ->
+ when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
+ OpenPgpApi.RESULT_CODE_SUCCESS -> {
+ try {
+ val showPassword = settings.getBoolean("show_password", true)
+ val showExtraContent = settings.getBoolean("show_extra_content", true)
+ val monoTypeface = Typeface.createFromAsset(assets, "fonts/sourcecodepro.ttf")
+ val entry = PasswordEntry(outputStream)
+
+ passwordEntry = entry
+
+ with(binding) {
+ if (entry.password.isEmpty()) {
+ passwordTextContainer.visibility = View.GONE
+ } else {
+ passwordTextContainer.visibility = View.VISIBLE
+ passwordText.typeface = monoTypeface
+ passwordText.setText(entry.password)
+ if (!showPassword) {
+ passwordText.transformationMethod = PasswordTransformationMethod.getInstance()
+ }
+ passwordTextContainer.setOnClickListener { copyPasswordToClipboard(entry.password) }
+ passwordText.setOnClickListener { copyPasswordToClipboard(entry.password) }
+ }
+
+ if (entry.hasExtraContent()) {
+ extraContentContainer.visibility = View.VISIBLE
+ extraContent.typeface = monoTypeface
+ extraContent.setText(entry.extraContentWithoutUsername)
+ if (!showExtraContent) {
+ extraContent.transformationMethod = PasswordTransformationMethod.getInstance()
+ }
+ extraContentContainer.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) }
+ extraContent.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) }
+
+ if (entry.hasUsername()) {
+ usernameText.typeface = monoTypeface
+ usernameText.setText(entry.username)
+ usernameTextContainer.setEndIconOnClickListener { copyTextToClipboard(entry.username) }
+ usernameTextContainer.visibility = View.VISIBLE
+ } else {
+ usernameTextContainer.visibility = View.GONE
+ }
+ }
+ }
+
+ if (settings.getBoolean("copy_on_decrypt", true)) {
+ copyPasswordToClipboard(entry.password)
+ }
+ } catch (e: Exception) {
+ e(e)
+ }
+ }
+ OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
+ val sender = getUserInteractionRequestIntent(result)
+ userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build())
+ }
+ OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt
new file mode 100644
index 00000000..94d5b68c
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package com.zeapo.pwdstore.crypto
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
+import androidx.core.content.edit
+import androidx.lifecycle.lifecycleScope
+import com.github.ajalt.timberkt.Timber
+import com.github.ajalt.timberkt.e
+import com.zeapo.pwdstore.utils.snackbar
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import me.msfjarvis.openpgpktx.util.OpenPgpApi
+import org.openintents.openpgp.IOpenPgpService2
+
+class GetKeyIdsActivity : BasePgpActivity() {
+
+ private val getKeyIds = registerForActivityResult(StartActivityForResult()) { getKeyIds() }
+
+ private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result ->
+ if (result.data == null) {
+ setResult(RESULT_CANCELED, null)
+ finish()
+ return@registerForActivityResult
+ }
+
+ when (result.resultCode) {
+ RESULT_OK -> getKeyIds(result.data)
+ RESULT_CANCELED -> {
+ setResult(RESULT_CANCELED, result.data)
+ finish()
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ bindToOpenKeychain(this, getKeyIds)
+ }
+
+ override fun onBound(service: IOpenPgpService2) {
+ super.onBound(service)
+ getKeyIds()
+ }
+
+ override fun onError(e: Exception) {
+ e(e)
+ }
+
+ /**
+ * Get the Key ids from OpenKeychain
+ */
+ private fun getKeyIds(receivedIntent: Intent? = null) {
+ val data = receivedIntent ?: Intent()
+ data.action = OpenPgpApi.ACTION_GET_KEY_IDS
+ lifecycleScope.launch(Dispatchers.IO) {
+ api?.executeApiAsync(data, null, null) { result ->
+ when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
+ OpenPgpApi.RESULT_CODE_SUCCESS -> {
+ try {
+ val ids = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)
+ ?: LongArray(0)
+ val keys = ids.map { it.toString() }.toSet()
+ // use Long
+ settings.edit { putStringSet("openpgp_key_ids_set", keys) }
+ snackbar(message = "PGP keys selected")
+ setResult(RESULT_OK)
+ finish()
+ } catch (e: Exception) {
+ Timber.e(e) { "An Exception occurred" }
+ }
+ }
+ OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
+ val sender = getUserInteractionRequestIntent(result)
+ userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build())
+ }
+ OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
new file mode 100644
index 00000000..7216a506
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
@@ -0,0 +1,296 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package com.zeapo.pwdstore.crypto
+
+import android.content.Intent
+import android.os.Bundle
+import android.text.InputType
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.core.widget.doOnTextChanged
+import androidx.lifecycle.lifecycleScope
+import com.github.ajalt.timberkt.e
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.zeapo.pwdstore.PasswordEntry
+import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
+import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
+import com.zeapo.pwdstore.databinding.PasswordCreationActivityBinding
+import com.zeapo.pwdstore.ui.dialogs.PasswordGeneratorDialogFragment
+import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment
+import com.zeapo.pwdstore.utils.PasswordRepository
+import com.zeapo.pwdstore.utils.commitChange
+import com.zeapo.pwdstore.utils.snackbar
+import com.zeapo.pwdstore.utils.viewBinding
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.eclipse.jgit.api.Git
+import me.msfjarvis.openpgpktx.util.OpenPgpApi
+import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.IOException
+
+class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
+
+ private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
+
+ private val suggestedName by lazy { intent.getStringExtra(EXTRA_FILE_NAME) }
+ private val suggestedPass by lazy { intent.getStringExtra(EXTRA_PASSWORD) }
+ private val suggestedExtra by lazy { intent.getStringExtra(EXTRA_EXTRA_CONTENT) }
+ private val shouldGeneratePassword by lazy { intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) }
+ private val doNothing = registerForActivityResult(StartActivityForResult()) {}
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ bindToOpenKeychain(this, doNothing)
+ title = if (intent.getBooleanExtra(EXTRA_EDITING, false))
+ getString(R.string.edit_password)
+ else
+ getString(R.string.new_password_title)
+ with(binding) {
+ setContentView(root)
+ generatePassword.setOnClickListener { generatePassword() }
+
+ category.apply {
+ if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) {
+ isEnabled = true
+ } else {
+ setBackgroundColor(getColor(android.R.color.transparent))
+ }
+ val path = getRelativePath(fullPath, repoPath)
+ // Keep empty path field visible if it is editable.
+ if (path.isEmpty() && !isEnabled)
+ visibility = View.GONE
+ else
+ setText(path)
+ }
+ suggestedName?.let { filename.setText(it) }
+ // Allow the user to quickly switch between storing the username as the filename or
+ // in the encrypted extras. This only makes sense if the directory structure is
+ // FileBased.
+ if (suggestedName == null &&
+ AutofillPreferences.directoryStructure(this@PasswordCreationActivity) ==
+ DirectoryStructure.FileBased
+ ) {
+ encryptUsername.apply {
+ visibility = View.VISIBLE
+ setOnClickListener {
+ if (isChecked) {
+ // User wants to enable username encryption, so we add it to the
+ // encrypted extras as the first line.
+ val username = filename.text.toString()
+ val extras = "username:$username\n${extraContent.text}"
+
+ filename.setText("")
+ extraContent.setText(extras)
+ } else {
+ // User wants to disable username encryption, so we extract the
+ // username from the encrypted extras and use it as the filename.
+ val entry = PasswordEntry("PASSWORD\n${extraContent.text}")
+ val username = entry.username
+
+ // username should not be null here by the logic in
+ // updateEncryptUsernameState, but it could still happen due to
+ // input lag.
+ if (username != null) {
+ filename.setText(username)
+ extraContent.setText(entry.extraContentWithoutUsername)
+ }
+ }
+ updateEncryptUsernameState()
+ }
+ }
+ listOf(filename, extraContent).forEach {
+ it.doOnTextChanged { _, _, _, _ -> updateEncryptUsernameState() }
+ }
+ }
+ suggestedPass?.let {
+ password.setText(it)
+ password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
+ }
+ suggestedExtra?.let { extraContent.setText(it) }
+ if (shouldGeneratePassword) {
+ generatePassword()
+ password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
+ }
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.pgp_handler_new_password, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home, R.id.cancel_password_add -> {
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ R.id.save_password -> encrypt()
+ R.id.save_and_copy_password -> encrypt(copy = true)
+ else -> return super.onOptionsItemSelected(item)
+ }
+ return true
+ }
+
+ private fun generatePassword() {
+ when (settings.getString("pref_key_pwgen_type", KEY_PWGEN_TYPE_CLASSIC)) {
+ KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment()
+ .show(supportFragmentManager, "generator")
+ KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment()
+ .show(supportFragmentManager, "xkpwgenerator")
+ }
+ }
+
+ private fun updateEncryptUsernameState() = with(binding) {
+ encryptUsername.apply {
+ if (visibility != View.VISIBLE)
+ return@with
+ val hasUsernameInFileName = filename.text.toString().isNotBlank()
+ // Use PasswordEntry to parse extras for username
+ val entry = PasswordEntry("PLACEHOLDER\n${extraContent.text}")
+ val hasUsernameInExtras = entry.hasUsername()
+ isEnabled = hasUsernameInFileName xor hasUsernameInExtras
+ isChecked = hasUsernameInExtras
+ }
+ }
+
+ /**
+ * Encrypts the password and the extra content
+ */
+ private fun encrypt(copy: Boolean = false) = with(binding) {
+ val editName = filename.text.toString().trim()
+ val editPass = password.text.toString()
+ val editExtra = extraContent.text.toString()
+
+ if (editName.isEmpty()) {
+ snackbar(message = resources.getString(R.string.file_toast_text))
+ return@with
+ }
+
+ if (editPass.isEmpty() && editExtra.isEmpty()) {
+ snackbar(message = resources.getString(R.string.empty_toast_text))
+ return@with
+ }
+
+ if (copy) {
+ copyPasswordToClipboard(editPass)
+ }
+
+ val data = Intent()
+ data.action = OpenPgpApi.ACTION_ENCRYPT
+
+ // EXTRA_KEY_IDS requires long[]
+ val longKeys = keyIDs.map { it.toLong() }
+ data.putExtra(OpenPgpApi.EXTRA_KEY_IDS, longKeys.toLongArray())
+ data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true)
+
+ val content = "$editPass\n$editExtra"
+ val inputStream = ByteArrayInputStream(content.toByteArray())
+ val outputStream = ByteArrayOutputStream()
+
+ val path = when {
+ // If we allowed the user to edit the relative path, we have to consider it here instead
+ // of fullPath.
+ category.isEnabled -> {
+ val editRelativePath = category.text.toString().trim()
+ if (editRelativePath.isEmpty()) {
+ snackbar(message = resources.getString(R.string.path_toast_text))
+ return
+ }
+ val passwordDirectory = File("$repoPath/${editRelativePath.trim('/')}")
+ if (!passwordDirectory.exists() && !passwordDirectory.mkdir()) {
+ snackbar(message = "Failed to create directory ${editRelativePath.trim('/')}")
+ return
+ }
+
+ "${passwordDirectory.path}/$editName.gpg"
+ }
+ else -> "$fullPath/$editName.gpg"
+ }
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ api?.executeApiAsync(data, inputStream, outputStream) { result ->
+ when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
+ OpenPgpApi.RESULT_CODE_SUCCESS -> {
+ try {
+ val file = File(path)
+ try {
+ file.outputStream().use {
+ it.write(outputStream.toByteArray())
+ }
+ } catch (e: IOException) {
+ e(e) { "Failed to write password file" }
+ setResult(RESULT_CANCELED)
+ MaterialAlertDialogBuilder(this@PasswordCreationActivity)
+ .setTitle(getString(R.string.password_creation_file_write_fail_title))
+ .setMessage(getString(R.string.password_creation_file_write_fail_message))
+ .setCancelable(false)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ finish()
+ }
+ .show()
+ }
+
+ val returnIntent = Intent()
+ returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path)
+ returnIntent.putExtra(RETURN_EXTRA_NAME, editName)
+ returnIntent.putExtra(RETURN_EXTRA_LONG_NAME, getLongName(fullPath, repoPath, editName))
+
+ if (shouldGeneratePassword) {
+ val directoryStructure =
+ AutofillPreferences.directoryStructure(applicationContext)
+ val entry = PasswordEntry(content)
+ returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password)
+ val username = PasswordEntry(content).username
+ ?: directoryStructure.getUsernameFor(file)
+ returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
+ }
+
+ val repo = PasswordRepository.getRepository(null)
+ if (repo != null) {
+ val status = Git(repo).status().call()
+ if (status.modified.isNotEmpty()) {
+ commitChange(
+ getString(
+ R.string.git_commit_edit_text,
+ getLongName(fullPath, repoPath, editName)
+ )
+ )
+ }
+ }
+ setResult(RESULT_OK, returnIntent)
+ finish()
+ } catch (e: Exception) {
+ e(e) { "An Exception occurred" }
+ }
+ }
+ OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val KEY_PWGEN_TYPE_CLASSIC = "classic"
+ private const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd"
+ const val RETURN_EXTRA_CREATED_FILE = "CREATED_FILE"
+ const val RETURN_EXTRA_NAME = "NAME"
+ const val RETURN_EXTRA_LONG_NAME = "LONG_NAME"
+ const val RETURN_EXTRA_USERNAME = "USERNAME"
+ const val RETURN_EXTRA_PASSWORD = "PASSWORD"
+ const val EXTRA_FILE_NAME = "FILENAME"
+ const val EXTRA_PASSWORD = "PASSWORD"
+ const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT"
+ const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD"
+ const val EXTRA_EDITING = "EDITING"
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt
deleted file mode 100644
index 77d7e70c..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt
+++ /dev/null
@@ -1,788 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package com.zeapo.pwdstore.crypto
-
-import android.app.PendingIntent
-import android.content.BroadcastReceiver
-import android.content.ClipData
-import android.content.ClipboardManager
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.IntentSender
-import android.content.SharedPreferences
-import android.graphics.Typeface
-import android.os.Build
-import android.os.Bundle
-import android.text.InputType
-import android.text.TextUtils
-import android.text.format.DateUtils
-import android.text.method.PasswordTransformationMethod
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.view.WindowManager
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.edit
-import androidx.core.content.getSystemService
-import androidx.core.widget.doOnTextChanged
-import androidx.lifecycle.lifecycleScope
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import androidx.preference.PreferenceManager
-import com.github.ajalt.timberkt.Timber.e
-import com.github.ajalt.timberkt.Timber.i
-import com.github.ajalt.timberkt.Timber.tag
-import com.google.android.material.snackbar.Snackbar
-import com.zeapo.pwdstore.ClipboardService
-import com.zeapo.pwdstore.PasswordEntry
-import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.UserPreference
-import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
-import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
-import com.zeapo.pwdstore.ui.dialogs.PasswordGeneratorDialogFragment
-import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment
-import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_category_decrypt
-import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_file
-import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_last_changed
-import kotlinx.android.synthetic.main.decrypt_layout.extra_content
-import kotlinx.android.synthetic.main.decrypt_layout.extra_content_container
-import kotlinx.android.synthetic.main.decrypt_layout.password_text
-import kotlinx.android.synthetic.main.decrypt_layout.password_text_container
-import kotlinx.android.synthetic.main.decrypt_layout.username_text
-import kotlinx.android.synthetic.main.decrypt_layout.username_text_container
-import kotlinx.android.synthetic.main.encrypt_layout.crypto_extra_edit
-import kotlinx.android.synthetic.main.encrypt_layout.crypto_password_category
-import kotlinx.android.synthetic.main.encrypt_layout.crypto_password_edit
-import kotlinx.android.synthetic.main.encrypt_layout.crypto_password_file_edit
-import kotlinx.android.synthetic.main.encrypt_layout.encrypt_username
-import kotlinx.android.synthetic.main.encrypt_layout.generate_password
-import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.launch
-import me.msfjarvis.openpgpktx.util.OpenPgpApi
-import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.ACTION_DECRYPT_VERIFY
-import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_CODE
-import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_CODE_ERROR
-import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_CODE_SUCCESS
-import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_CODE_USER_INTERACTION_REQUIRED
-import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_ERROR
-import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_INTENT
-import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
-import org.openintents.openpgp.IOpenPgpService2
-import org.openintents.openpgp.OpenPgpError
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-import java.io.File
-import java.nio.charset.Charset
-
-class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
- private val clipboard by lazy { getSystemService<ClipboardManager>() }
- private var passwordEntry: PasswordEntry? = null
- private var api: OpenPgpApi? = null
-
- private var editName: String? = null
- private var editPass: String? = null
- private var editExtra: String? = null
-
- private val suggestedName by lazy { intent.getStringExtra("SUGGESTED_NAME") }
- private val suggestedPass by lazy { intent.getStringExtra("SUGGESTED_PASS") }
- private val suggestedExtra by lazy { intent.getStringExtra("SUGGESTED_EXTRA") }
- private val shouldGeneratePassword by lazy { intent.getBooleanExtra("GENERATE_PASSWORD", false) }
-
- private val operation: String by lazy { intent.getStringExtra("OPERATION") }
- private val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") }
-
- private val fullPath: String by lazy { intent.getStringExtra("FILE_PATH") }
- private val name: String by lazy { File(fullPath).nameWithoutExtension }
- private val lastChangedString: CharSequence by lazy {
- getLastChangedString(
- intent.getLongExtra(
- "LAST_CHANGED_TIMESTAMP",
- -1L
- )
- )
- }
- private val relativeParentPath: String by lazy { getParentPath(fullPath, repoPath) }
-
- val settings: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
- private val keyIDs get() = _keyIDs
- private var _keyIDs = emptySet<String>()
- private var serviceConnection: OpenPgpServiceConnection? = null
- private var delayTask: DelayShow? = null
- private val receiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- delayTask?.doOnPostExecute()
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
- tag(TAG)
-
- // some persistence
- _keyIDs = settings.getStringSet("openpgp_key_ids_set", null) ?: emptySet()
- val providerPackageName = settings.getString("openpgp_provider_list", "")
-
- if (TextUtils.isEmpty(providerPackageName)) {
- showSnackbar(resources.getString(R.string.provider_toast_text), Snackbar.LENGTH_LONG)
- val intent = Intent(this, UserPreference::class.java)
- startActivityForResult(intent, OPEN_PGP_BOUND)
- } else {
- // bind to service
- serviceConnection = OpenPgpServiceConnection(this, providerPackageName, this)
- serviceConnection?.bindToService()
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- }
-
- when (operation) {
- "DECRYPT", "EDIT" -> {
- setContentView(R.layout.decrypt_layout)
- crypto_password_category_decrypt.text = relativeParentPath
- crypto_password_file.text = name
- crypto_password_file.setOnLongClickListener {
- val clipboard = clipboard ?: return@setOnLongClickListener false
- val clip = ClipData.newPlainText("pgp_handler_result_pm", name)
- clipboard.setPrimaryClip(clip)
- showSnackbar(resources.getString(R.string.clipboard_copied_text))
- true
- }
-
- crypto_password_last_changed.text = try {
- resources.getString(R.string.last_changed, lastChangedString)
- } catch (e: RuntimeException) {
- showSnackbar(getString(R.string.get_last_changed_failed))
- ""
- }
- }
- "ENCRYPT" -> {
- setContentView(R.layout.encrypt_layout)
-
- generate_password?.setOnClickListener {
- generatePassword()
- }
-
- title = getString(R.string.new_password_title)
- crypto_password_category.apply {
- // If the activity has been provided with suggested info or is meant to generate
- // a password, we allow the user to edit the path, otherwise we style the
- // EditText like a TextView.
- if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) {
- isEnabled = true
- } else {
- setBackgroundColor(getColor(android.R.color.transparent))
- }
- val path = getRelativePath(fullPath, repoPath)
- // Keep empty path field visible if it is editable.
- if (path.isEmpty() && !isEnabled)
- visibility = View.GONE
- else
- setText(path)
- }
- suggestedName?.let { crypto_password_file_edit.setText(it) }
- // Allow the user to quickly switch between storing the username as the filename or
- // in the encrypted extras. This only makes sense if the directory structure is
- // FileBased.
- if (suggestedName != null &&
- AutofillPreferences.directoryStructure(this) == DirectoryStructure.FileBased
- ) {
- encrypt_username.apply {
- visibility = View.VISIBLE
- setOnClickListener {
- if (isChecked) {
- // User wants to enable username encryption, so we add it to the
- // encrypted extras as the first line.
- val username = crypto_password_file_edit.text!!.toString()
- val extras = "username:$username\n${crypto_extra_edit.text!!}"
-
- crypto_password_file_edit.setText("")
- crypto_extra_edit.setText(extras)
- } else {
- // User wants to disable username encryption, so we extract the
- // username from the encrypted extras and use it as the filename.
- val entry = PasswordEntry("PASSWORD\n${crypto_extra_edit.text!!}")
- val username = entry.username
-
- // username should not be null here by the logic in
- // updateEncryptUsernameState, but it could still happen due to
- // input lag.
- if (username != null) {
- crypto_password_file_edit.setText(username)
- crypto_extra_edit.setText(entry.extraContentWithoutUsername)
- }
- }
- updateEncryptUsernameState()
- }
- }
- crypto_password_file_edit.doOnTextChanged { _, _, _, _ -> updateEncryptUsernameState() }
- crypto_extra_edit.doOnTextChanged { _, _, _, _ -> updateEncryptUsernameState() }
- updateEncryptUsernameState()
- }
- suggestedPass?.let {
- crypto_password_edit.setText(it)
- crypto_password_edit.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
- }
- suggestedExtra?.let { crypto_extra_edit.setText(it) }
- if (shouldGeneratePassword) {
- generatePassword()
- crypto_password_edit.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
- }
- }
- }
- }
-
- private fun updateEncryptUsernameState() {
- encrypt_username.apply {
- if (visibility != View.VISIBLE)
- return
- val hasUsernameInFileName = crypto_password_file_edit.text!!.toString().isNotBlank()
- // Use PasswordEntry to parse extras for username
- val entry = PasswordEntry("PLACEHOLDER\n${crypto_extra_edit.text!!}")
- val hasUsernameInExtras = entry.hasUsername()
- isEnabled = hasUsernameInFileName xor hasUsernameInExtras
- isChecked = hasUsernameInExtras
- }
- }
-
- override fun onResume() {
- super.onResume()
- LocalBroadcastManager.getInstance(this).registerReceiver(receiver, IntentFilter(ACTION_CLEAR))
- }
-
- private fun generatePassword() {
- when (settings.getString("pref_key_pwgen_type", KEY_PWGEN_TYPE_CLASSIC)) {
- KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment()
- .show(supportFragmentManager, "generator")
- KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment()
- .show(supportFragmentManager, "xkpwgenerator")
- }
- }
-
- override fun onStop() {
- LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
- super.onStop()
- }
-
- override fun onDestroy() {
- super.onDestroy()
- serviceConnection?.unbindFromService()
- }
-
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
- // Inflate the menu; this adds items to the action bar if it is present.
- // Do not use the value `operation` in this case as it is not valid when editing
- val menuId = when (intent.getStringExtra("OPERATION")) {
- "ENCRYPT", "EDIT" -> R.menu.pgp_handler_new_password
- "DECRYPT" -> R.menu.pgp_handler
- else -> R.menu.pgp_handler
- }
-
- menuInflater.inflate(menuId, menu)
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.crypto_cancel_add, android.R.id.home -> finish()
- R.id.copy_password -> copyPasswordToClipBoard()
- R.id.share_password_as_plaintext -> shareAsPlaintext()
- R.id.edit_password -> editPassword()
- R.id.crypto_confirm_add -> encrypt()
- R.id.crypto_confirm_add_and_copy -> encrypt(true)
- else -> return super.onOptionsItemSelected(item)
- }
- return true
- }
-
- /**
- * Shows a simple toast message
- */
- private fun showSnackbar(message: String, length: Int = Snackbar.LENGTH_SHORT) {
- runOnUiThread { Snackbar.make(findViewById(android.R.id.content), message, length).show() }
- }
-
- /**
- * Handle the case where OpenKeychain returns that it needs to interact with the user
- *
- * @param result The intent returned by OpenKeychain
- * @param requestCode The code we'd like to use to identify the behaviour
- */
- private fun handleUserInteractionRequest(result: Intent, requestCode: Int) {
- i { "RESULT_CODE_USER_INTERACTION_REQUIRED" }
-
- val pi: PendingIntent? = result.getParcelableExtra(RESULT_INTENT)
- try {
- this@PgpActivity.startIntentSenderFromChild(
- this@PgpActivity, pi?.intentSender, requestCode,
- null, 0, 0, 0
- )
- } catch (e: IntentSender.SendIntentException) {
- e(e) { "SendIntentException" }
- }
- }
-
- /**
- * Handle the error returned by OpenKeychain
- *
- * @param result The intent returned by OpenKeychain
- */
- private fun handleError(result: Intent) {
- // TODO show what kind of error it is
- /* For example:
- * No suitable key found -> no key in OpenKeyChain
- *
- * Check in open-pgp-lib how their definitions and error code
- */
- val error: OpenPgpError? = result.getParcelableExtra(RESULT_ERROR)
- if (error != null) {
- showSnackbar("Error from OpenKeyChain : " + error.message)
- e { "onError getErrorId: ${error.errorId}" }
- e { "onError getMessage: ${error.message}" }
- }
- }
-
- private fun initOpenPgpApi() {
- api = api ?: OpenPgpApi(this, serviceConnection!!.service!!)
- }
-
- private fun decryptAndVerify(receivedIntent: Intent? = null) {
- val data = receivedIntent ?: Intent()
- data.action = ACTION_DECRYPT_VERIFY
-
- val iStream = File(fullPath).inputStream()
- val oStream = ByteArrayOutputStream()
-
- lifecycleScope.launch(IO) {
- api?.executeApiAsync(data, iStream, oStream) { result ->
- when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) {
- RESULT_CODE_SUCCESS -> {
- try {
- val showPassword = settings.getBoolean("show_password", true)
- val showExtraContent = settings.getBoolean("show_extra_content", true)
-
- password_text_container.visibility = View.VISIBLE
-
- val monoTypeface = Typeface.createFromAsset(assets, "fonts/sourcecodepro.ttf")
- val entry = PasswordEntry(oStream)
-
- passwordEntry = entry
-
- if (intent.getStringExtra("OPERATION") == "EDIT") {
- editPassword()
- return@executeApiAsync
- }
-
- if (entry.password.isEmpty()) {
- password_text_container.visibility = View.GONE
- } else {
- password_text_container.visibility = View.VISIBLE
- password_text.setText(entry.password)
- if (!showPassword) {
- password_text.transformationMethod = PasswordTransformationMethod.getInstance()
- }
- password_text_container.setOnClickListener { copyPasswordToClipBoard() }
- password_text.setOnClickListener { copyPasswordToClipBoard() }
- }
-
- if (entry.hasExtraContent()) {
- extra_content_container.visibility = View.VISIBLE
- extra_content.typeface = monoTypeface
- extra_content.setText(entry.extraContentWithoutUsername)
- if (!showExtraContent) {
- extra_content.transformationMethod = PasswordTransformationMethod.getInstance()
- }
- extra_content_container.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) }
- extra_content.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) }
-
- if (entry.hasUsername()) {
- username_text.typeface = monoTypeface
- username_text.setText(entry.username)
- username_text_container.setEndIconOnClickListener { copyTextToClipboard(entry.username!!) }
- username_text_container.visibility = View.VISIBLE
- } else {
- username_text_container.visibility = View.GONE
- }
- }
-
- if (settings.getBoolean("copy_on_decrypt", true)) {
- copyPasswordToClipBoard()
- }
- } catch (e: Exception) {
- e(e) { "An Exception occurred" }
- }
- }
- RESULT_CODE_USER_INTERACTION_REQUIRED -> handleUserInteractionRequest(result, REQUEST_DECRYPT)
- RESULT_CODE_ERROR -> handleError(result)
- }
- }
- }
- }
-
- /**
- * Encrypts the password and the extra content
- */
- private fun encrypt(copy: Boolean = false) {
- editName = crypto_password_file_edit.text.toString().trim()
- editPass = crypto_password_edit.text.toString()
- editExtra = crypto_extra_edit.text.toString()
-
- if (editName?.isEmpty() == true) {
- showSnackbar(resources.getString(R.string.file_toast_text))
- return
- }
-
- if (editPass?.isEmpty() == true && editExtra?.isEmpty() == true) {
- showSnackbar(resources.getString(R.string.empty_toast_text))
- return
- }
-
- if (copy) {
- copyPasswordToClipBoard()
- }
-
- val data = Intent()
- data.action = OpenPgpApi.ACTION_ENCRYPT
-
- // EXTRA_KEY_IDS requires long[]
- val longKeys = keyIDs.map { it.toLong() }
- data.putExtra(OpenPgpApi.EXTRA_KEY_IDS, longKeys.toLongArray())
- data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true)
-
- // TODO Check if we could use PasswordEntry to generate the file
- val content = "$editPass\n$editExtra"
- val iStream = ByteArrayInputStream(content.toByteArray(Charset.forName("UTF-8")))
- val oStream = ByteArrayOutputStream()
-
- val path = when {
- intent.getBooleanExtra("fromDecrypt", false) -> fullPath
- // If we allowed the user to edit the relative path, we have to consider it here instead
- // of fullPath.
- crypto_password_category.isEnabled -> {
- val editRelativePath = crypto_password_category.text!!.toString().trim()
- if (editRelativePath.isEmpty()) {
- showSnackbar(resources.getString(R.string.path_toast_text))
- return
- }
- "$repoPath/${editRelativePath.trim('/')}/$editName.gpg"
- }
- else -> "$fullPath/$editName.gpg"
- }
-
- lifecycleScope.launch(IO) {
- api?.executeApiAsync(data, iStream, oStream) { result ->
- when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) {
- RESULT_CODE_SUCCESS -> {
- try {
- // TODO This might fail, we should check that the write is successful
- val file = File(path)
- val outputStream = file.outputStream()
- outputStream.write(oStream.toByteArray())
- outputStream.close()
-
- val returnIntent = Intent()
- returnIntent.putExtra("CREATED_FILE", path)
- returnIntent.putExtra("NAME", editName)
- returnIntent.putExtra("LONG_NAME", getLongName(fullPath, repoPath, editName!!))
-
- // if coming from decrypt screen->edit button
- if (intent.getBooleanExtra("fromDecrypt", false)) {
- returnIntent.putExtra("OPERATION", "EDIT")
- returnIntent.putExtra("needCommit", true)
- }
-
- if (shouldGeneratePassword) {
- val directoryStructure =
- AutofillPreferences.directoryStructure(applicationContext)
- val entry = PasswordEntry(content)
- returnIntent.putExtra("PASSWORD", entry.password)
- val username = PasswordEntry(content).username
- ?: directoryStructure.getUsernameFor(file)
- returnIntent.putExtra("USERNAME", username)
- }
-
- setResult(RESULT_OK, returnIntent)
- finish()
- } catch (e: Exception) {
- e(e) { "An Exception occurred" }
- }
- }
- RESULT_CODE_ERROR -> handleError(result)
- }
- }
- }
- }
-
- /**
- * Opens EncryptActivity with the information for this file to be edited
- */
- private fun editPassword() {
- setContentView(R.layout.encrypt_layout)
- generate_password?.setOnClickListener {
- when (settings.getString("pref_key_pwgen_type", KEY_PWGEN_TYPE_CLASSIC)) {
- KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment()
- .show(supportFragmentManager, "generator")
- KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment()
- .show(supportFragmentManager, "xkpwgenerator")
- }
- }
-
- title = getString(R.string.edit_password_title)
-
- val monoTypeface = Typeface.createFromAsset(assets, "fonts/sourcecodepro.ttf")
- crypto_password_edit.setText(passwordEntry?.password)
- crypto_password_edit.typeface = monoTypeface
- crypto_extra_edit.setText(passwordEntry?.extraContent)
- crypto_extra_edit.typeface = monoTypeface
-
- crypto_password_category.setText(relativeParentPath)
- crypto_password_file_edit.setText(name)
- crypto_password_file_edit.isEnabled = false
-
- delayTask?.cancelAndSignal(true)
-
- val data = Intent(this, PgpActivity::class.java)
- data.putExtra("OPERATION", "EDIT")
- data.putExtra("fromDecrypt", true)
- intent = data
- invalidateOptionsMenu()
- }
-
- /**
- * Get the Key ids from OpenKeychain
- */
- private fun getKeyIds(receivedIntent: Intent? = null) {
- val data = receivedIntent ?: Intent()
- data.action = OpenPgpApi.ACTION_GET_KEY_IDS
- lifecycleScope.launch(IO) {
- api?.executeApiAsync(data, null, null) { result ->
- when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) {
- RESULT_CODE_SUCCESS -> {
- try {
- val ids = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)
- ?: LongArray(0)
- val keys = ids.map { it.toString() }.toSet()
-
- // use Long
- settings.edit { putStringSet("openpgp_key_ids_set", keys) }
-
- showSnackbar("PGP keys selected")
-
- setResult(RESULT_OK)
- finish()
- } catch (e: Exception) {
- e(e) { "An Exception occurred" }
- }
- }
- RESULT_CODE_USER_INTERACTION_REQUIRED -> handleUserInteractionRequest(result, REQUEST_KEY_ID)
- RESULT_CODE_ERROR -> handleError(result)
- }
- }
- }
- }
-
- override fun onError(e: Exception) {}
-
- /**
- * The action to take when the PGP service is bound
- */
- override fun onBound(service: IOpenPgpService2) {
- initOpenPgpApi()
- when (operation) {
- "EDIT", "DECRYPT" -> decryptAndVerify()
- "GET_KEY_ID" -> getKeyIds()
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
-
- if (data == null) {
- setResult(RESULT_CANCELED, null)
- finish()
- return
- }
-
- // try again after user interaction
- if (resultCode == RESULT_OK) {
- when (requestCode) {
- REQUEST_DECRYPT -> decryptAndVerify(data)
- REQUEST_KEY_ID -> getKeyIds(data)
- else -> {
- setResult(RESULT_OK)
- finish()
- }
- }
- } else if (resultCode == RESULT_CANCELED) {
- setResult(RESULT_CANCELED, data)
- finish()
- }
- }
-
- private fun copyPasswordToClipBoard() {
- val clipboard = clipboard ?: return
- val pass = passwordEntry?.password
- val clip = ClipData.newPlainText("pgp_handler_result_pm", pass)
- clipboard.setPrimaryClip(clip)
-
- var clearAfter = 45
- try {
- clearAfter = Integer.parseInt(settings.getString("general_show_time", "45") as String)
- } catch (e: NumberFormatException) {
- // ignore and keep default
- }
-
- if (clearAfter != 0) {
- setTimer()
- showSnackbar(resources.getString(R.string.clipboard_password_toast_text, clearAfter))
- } else {
- showSnackbar(resources.getString(R.string.clipboard_password_no_clear_toast_text))
- }
- }
-
- private fun copyTextToClipboard(text: String) {
- val clipboard = clipboard ?: return
- val clip = ClipData.newPlainText("pgp_handler_result_pm", text)
- clipboard.setPrimaryClip(clip)
- showSnackbar(resources.getString(R.string.clipboard_copied_text))
- }
-
- private fun shareAsPlaintext() {
- val sendIntent = Intent()
- sendIntent.action = Intent.ACTION_SEND
- sendIntent.putExtra(Intent.EXTRA_TEXT, passwordEntry?.password)
- sendIntent.type = "text/plain"
- startActivity(
- Intent.createChooser(
- sendIntent,
- resources.getText(R.string.send_plaintext_password_to)
- )
- ) // Always show a picker to give the user a chance to cancel
- }
-
- private fun setTimer() {
-
- // make sure to cancel any running tasks as soon as possible
- // if the previous task is still running, do not ask it to clear the password
- delayTask?.cancelAndSignal(true)
-
- // launch a new one
- delayTask = DelayShow()
- delayTask?.execute()
- }
-
- /**
- * Gets a relative string describing when this shape was last changed
- * (e.g. "one hour ago")
- */
- private fun getLastChangedString(timeStamp: Long): CharSequence {
- if (timeStamp < 0) {
- throw RuntimeException()
- }
-
- return DateUtils.getRelativeTimeSpanString(this, timeStamp, true)
- }
-
- @Suppress("StaticFieldLeak")
- inner class DelayShow {
-
- private var skip = false
- private var service: Intent? = null
- private var showTime: Int = 0
-
- // Custom cancellation that can be triggered from another thread.
- //
- // This signals the DelayShow task to stop and avoids it having
- // to poll the AsyncTask.isCancelled() excessively. If skipClearing
- // is true, the cancelled task won't clear the clipboard.
- fun cancelAndSignal(skipClearing: Boolean) {
- skip = skipClearing
- if (service != null) {
- stopService(service)
- service = null
- }
- }
-
- fun execute() {
- service = Intent(this@PgpActivity, ClipboardService::class.java).also {
- it.action = ACTION_START
- }
- doOnPreExecute()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- startForegroundService(service)
- } else {
- startService(service)
- }
- }
-
- private fun doOnPreExecute() {
- showTime = try {
- Integer.parseInt(settings.getString("general_show_time", "45") as String)
- } catch (e: NumberFormatException) {
- 45
- }
- password_text_container?.visibility = View.VISIBLE
- if (extra_content?.text?.isNotEmpty() == true)
- extra_content_container?.visibility = View.VISIBLE
- }
-
- fun doOnPostExecute() {
- if (skip) return
-
- if (password_text != null) {
- passwordEntry = null
- extra_content_container.visibility = View.INVISIBLE
- password_text_container.visibility = View.INVISIBLE
- finish()
- }
- }
- }
-
- companion object {
- const val OPEN_PGP_BOUND = 101
- const val REQUEST_DECRYPT = 202
- const val REQUEST_KEY_ID = 203
-
- private const val ACTION_CLEAR = "ACTION_CLEAR_CLIPBOARD"
- private const val ACTION_START = "ACTION_START_CLIPBOARD_TIMER"
-
- const val TAG = "PgpActivity"
-
- const val KEY_PWGEN_TYPE_CLASSIC = "classic"
- const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd"
-
- /**
- * Gets the relative path to the repository
- */
- fun getRelativePath(fullPath: String, repositoryPath: String): String =
- fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/")
-
- /**
- * Gets the Parent path, relative to the repository
- */
- fun getParentPath(fullPath: String, repositoryPath: String): String {
- val relativePath = getRelativePath(fullPath, repositoryPath)
- val index = relativePath.lastIndexOf("/")
- return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace("/+".toRegex(), "/")
- }
-
- /**
- * /path/to/store/social/facebook.gpg -> social/facebook
- */
- @JvmStatic
- fun getLongName(fullPath: String, repositoryPath: String, basename: String): String {
- var relativePath = getRelativePath(fullPath, repositoryPath)
- return if (relativePath.isNotEmpty() && relativePath != "/") {
- // remove preceding '/'
- relativePath = relativePath.substring(1)
- if (relativePath.endsWith('/')) {
- relativePath + basename
- } else {
- "$relativePath/$basename"
- }
- } else {
- basename
- }
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitAsyncTask.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitAsyncTask.kt
index 124f2d0a..a2b4e2a8 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitAsyncTask.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitAsyncTask.kt
@@ -32,7 +32,9 @@ class GitAsyncTask(
activity: Activity,
private val refreshListOnEnd: Boolean,
private val operation: GitOperation,
- private val finishWithResultOnEnd: Intent?) : AsyncTask<GitCommand<*>, Int, GitAsyncTask.Result>() {
+ private val finishWithResultOnEnd: Intent?,
+ private val silentlyExecute: Boolean = false
+) : AsyncTask<GitCommand<*>, Int, GitAsyncTask.Result>() {
private val activityWeakReference: WeakReference<Activity> = WeakReference(activity)
private val activity: Activity?
@@ -46,6 +48,7 @@ class GitAsyncTask(
}
override fun onPreExecute() {
+ if (silentlyExecute) return
dialog.run {
setMessage(activity!!.resources.getString(R.string.running_dialog_text))
setCancelable(false)
@@ -141,7 +144,7 @@ class GitAsyncTask(
}
override fun onPostExecute(maybeResult: Result?) {
- dialog.dismiss()
+ if (!silentlyExecute) dialog.dismiss()
when (val result = maybeResult ?: Result.Err(IOException("Unexpected error"))) {
is Result.Err -> {
if (isExplicitlyUserInitiatedError(result.err)) {
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
index 1eac569b..1f5494fd 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
@@ -58,7 +58,7 @@ class GitServerConfigActivity : BaseGitActivity() {
ConnectionMode.OpenKeychain -> check(R.id.connection_mode_open_keychain)
ConnectionMode.None -> uncheck(checkedButtonId)
}
- addOnButtonCheckedListener { group, _, _ ->
+ addOnButtonCheckedListener { _, _, _ ->
when (checkedButtonId) {
R.id.connection_mode_ssh_key -> connectionMode = ConnectionMode.SshKey
R.id.connection_mode_open_keychain -> connectionMode = ConnectionMode.OpenKeychain
diff --git a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt
index 5d35123d..8f4fbf84 100644
--- a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt
@@ -7,15 +7,14 @@ package com.zeapo.pwdstore.sshkeygen
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.ClipData
-import android.content.ClipboardManager
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
-import androidx.core.content.getSystemService
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.utils.clipboard
import java.io.File
class ShowSshKeyFragment : DialogFragment() {
@@ -39,8 +38,7 @@ class ShowSshKeyFragment : DialogFragment() {
ad.setOnShowListener {
val b = ad.getButton(AlertDialog.BUTTON_NEUTRAL)
b.setOnClickListener {
- val clipboard = activity.getSystemService<ClipboardManager>()
- ?: return@setOnClickListener
+ val clipboard = activity.clipboard ?: return@setOnClickListener
val clip = ClipData.newPlainText("public key", publicKey.text.toString())
clipboard.setPrimaryClip(clip)
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt
index 659c3f54..8f452dca 100644
--- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt
@@ -46,7 +46,7 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
val passwordText: AppCompatTextView = view.findViewById(R.id.passwordText)
passwordText.typeface = monoTypeface
builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ ->
- val edit = callingActivity.findViewById<EditText>(R.id.crypto_password_edit)
+ val edit = callingActivity.findViewById<EditText>(R.id.password)
edit.setText(passwordText.text)
}
builder.setNeutralButton(resources.getString(R.string.dialog_cancel)) { _, _ -> }
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt
index 825a8890..52bc52c1 100644
--- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt
@@ -92,7 +92,7 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ ->
setPreferences()
- val edit = callingActivity.findViewById<EditText>(R.id.crypto_password_edit)
+ val edit = callingActivity.findViewById<EditText>(R.id.password)
edit.setText(passwordText.text)
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/ClipboardUtils.kt b/app/src/main/java/com/zeapo/pwdstore/utils/ClipboardUtils.kt
deleted file mode 100644
index 2e408bfd..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/utils/ClipboardUtils.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package com.zeapo.pwdstore.utils
-
-import android.content.ClipData
-import android.content.ClipboardManager
-import com.github.ajalt.timberkt.d
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-object ClipboardUtils {
-
- suspend fun clearClipboard(clipboard: ClipboardManager, deepClear: Boolean = false) {
- d { "Clearing the clipboard" }
- val clip = ClipData.newPlainText("pgp_handler_result_pm", "")
- clipboard.setPrimaryClip(clip)
- if (deepClear) {
- withContext(Dispatchers.IO) {
- repeat(20) {
- val count = (it * 500).toString()
- clipboard.setPrimaryClip(ClipData.newPlainText(count, count))
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
index d10bdaab..ff108b30 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
@@ -4,7 +4,10 @@
*/
package com.zeapo.pwdstore.utils
+import android.app.Activity
+import android.content.ClipboardManager
import android.content.Context
+import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.util.TypedValue
@@ -16,7 +19,13 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService
import androidx.security.crypto.EncryptedSharedPreferences
-import androidx.security.crypto.MasterKeys
+import androidx.security.crypto.MasterKey
+import com.github.ajalt.timberkt.d
+import com.google.android.material.snackbar.Snackbar
+import com.zeapo.pwdstore.git.GitAsyncTask
+import com.zeapo.pwdstore.git.GitOperation
+import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory
+import org.eclipse.jgit.api.Git
import java.io.File
infix fun Int.hasFlag(flag: Int): Boolean {
@@ -33,6 +42,16 @@ fun CharArray.clear() {
}
}
+val Context.clipboard get() = getSystemService<ClipboardManager>()
+
+fun Activity.snackbar(
+ view: View = findViewById(android.R.id.content),
+ message: String,
+ length: Int = Snackbar.LENGTH_SHORT
+) {
+ Snackbar.make(view, message, length).show()
+}
+
fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList()
fun Context.resolveAttribute(attr: Int): Int {
@@ -42,17 +61,39 @@ fun Context.resolveAttribute(attr: Int): Int {
}
fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
- val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
- val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
+ val masterKeyAlias = MasterKey.Builder(applicationContext)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build()
return EncryptedSharedPreferences.create(
+ applicationContext,
fileName,
masterKeyAlias,
- this,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
+fun Activity.commitChange(message: String, finishWithResultOnEnd: Intent? = null) {
+ if (!PasswordRepository.isGitRepo()) {
+ if (finishWithResultOnEnd != null) {
+ setResult(Activity.RESULT_OK, finishWithResultOnEnd)
+ finish()
+ }
+ return
+ }
+ object : GitOperation(getRepositoryDirectory(this@commitChange), this@commitChange) {
+ override fun execute() {
+ d { "Comitting with message: '$message'" }
+ val git = Git(repository)
+ val task = GitAsyncTask(this@commitChange, true, this, finishWithResultOnEnd, silentlyExecute = true)
+ task.execute(
+ git.add().addFilepattern("."),
+ git.commit().setAll(true).setMessage(message)
+ )
+ }
+ }.execute()
+}
+
/**
* Extension function for [AlertDialog] that requests focus for the
* view whose id is [id]. Solution based on a StackOverflow
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt
index 903f6402..5ca95d31 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt
@@ -4,7 +4,7 @@
*/
package com.zeapo.pwdstore.utils
-import com.zeapo.pwdstore.crypto.PgpActivity
+import com.zeapo.pwdstore.crypto.BasePgpActivity
import java.io.File
data class PasswordItem(
@@ -19,7 +19,7 @@ data class PasswordItem(
.replace(rootDir.absolutePath, "")
.replace(file.name, "")
- val longName = PgpActivity.getLongName(
+ val longName = BasePgpActivity.getLongName(
fullPathToParent,
rootDir.absolutePath,
toString())