diff options
author | Harsh Shandilya <msfjarvis@gmail.com> | 2020-06-12 14:58:15 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-12 20:28:15 +0530 |
commit | d8231e112afb501c43041a6f839ab8285f400f77 (patch) | |
tree | 6785a8d0c6a70a3017ce5d62fe6b8234d5564f07 /app/src/main/java/com | |
parent | bf33fb2c88a208931340201e95a524b274d70b31 (diff) |
Break down PGP Activity into focused sections (#776)
Diffstat (limited to 'app/src/main/java/com')
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()) |