From d8231e112afb501c43041a6f839ab8285f400f77 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 12 Jun 2020 14:58:15 +0000 Subject: Break down PGP Activity into focused sections (#776) --- app/src/main/AndroidManifest.xml | 21 +- .../java/com/zeapo/pwdstore/ClipboardService.kt | 30 +- .../main/java/com/zeapo/pwdstore/LaunchActivity.kt | 11 +- .../java/com/zeapo/pwdstore/PasswordFragment.kt | 13 +- .../main/java/com/zeapo/pwdstore/PasswordStore.kt | 62 +- .../com/zeapo/pwdstore/SelectFolderActivity.kt | 19 +- .../main/java/com/zeapo/pwdstore/UserPreference.kt | 69 +- .../autofill/AutofillPreferenceActivity.kt | 31 +- .../autofill/oreo/ui/AutofillSaveActivity.kt | 19 +- .../com/zeapo/pwdstore/crypto/BasePgpActivity.kt | 270 +++++++ .../com/zeapo/pwdstore/crypto/DecryptActivity.kt | 202 ++++++ .../com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt | 89 +++ .../pwdstore/crypto/PasswordCreationActivity.kt | 296 ++++++++ .../java/com/zeapo/pwdstore/crypto/PgpActivity.kt | 788 --------------------- .../java/com/zeapo/pwdstore/git/GitAsyncTask.kt | 7 +- .../zeapo/pwdstore/git/GitServerConfigActivity.kt | 2 +- .../zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt | 6 +- .../ui/dialogs/PasswordGeneratorDialogFragment.kt | 2 +- .../dialogs/XkPasswordGeneratorDialogFragment.kt | 2 +- .../com/zeapo/pwdstore/utils/ClipboardUtils.kt | 28 - .../java/com/zeapo/pwdstore/utils/Extensions.kt | 49 +- .../java/com/zeapo/pwdstore/utils/PasswordItem.kt | 4 +- .../lib/publicsuffixlist/ext/ByteArray.kt | 2 +- app/src/main/res/layout/decrypt_layout.xml | 12 +- app/src/main/res/layout/encrypt_layout.xml | 97 --- .../main/res/layout/password_creation_activity.xml | 97 +++ app/src/main/res/menu/context_pass.xml | 6 - app/src/main/res/menu/pgp_handler_new_password.xml | 6 +- app/src/main/res/values-ar/strings.xml | 9 - app/src/main/res/values-cs/strings.xml | 15 +- app/src/main/res/values-de/strings.xml | 12 - app/src/main/res/values-es/strings.xml | 13 - app/src/main/res/values-fr/strings.xml | 14 - app/src/main/res/values-ja/strings.xml | 10 - app/src/main/res/values-night/colors.xml | 4 - app/src/main/res/values-ru/strings.xml | 16 - app/src/main/res/values-zh-rCN/strings.xml | 10 - app/src/main/res/values-zh-rTW/strings.xml | 10 - app/src/main/res/values/arrays.xml | 15 - app/src/main/res/values/colors.xml | 4 - app/src/main/res/values/dimens.xml | 1 - app/src/main/res/values/strings.xml | 32 +- 42 files changed, 1155 insertions(+), 1250 deletions(-) create mode 100644 app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/ClipboardUtils.kt delete mode 100644 app/src/main/res/layout/encrypt_layout.xml create mode 100644 app/src/main/res/layout/password_creation_activity.xml (limited to 'app/src') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8463c352..b0c3193d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,6 +64,22 @@ android:label="@string/action_settings" android:parentActivityName=".PasswordStore" /> + + + + + + - () + 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 private lateinit var oreoAutofillDependencies: List 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() { - val weakReference = WeakReference(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(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() + 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) { + 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() } - 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() - 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, Int, GitAsyncTask.Result>() { + private val finishWithResultOnEnd: Intent?, + private val silentlyExecute: Boolean = false +) : AsyncTask, Int, GitAsyncTask.Result>() { private val activityWeakReference: WeakReference = 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() - ?: 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(R.id.crypto_password_edit) + val edit = callingActivity.findViewById(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(R.id.crypto_password_edit) + val edit = callingActivity.findViewById(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() + +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()) diff --git a/app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt b/app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt index 5abb9154..43fb7ab1 100644 --- a/app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt +++ b/app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt @@ -15,7 +15,7 @@ private const val BITMASK = 0xff.toByte() * Performs a binary search for the provided [labels] on the [ByteArray]'s data. * * This algorithm is based on OkHttp's PublicSuffixDatabase class: - * https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java + * https://github.com/square/okhttp/blob/1977136/okhttp/src/main/kotlin/okhttp3/internal/publicsuffix/PublicSuffixDatabase.kt */ @Suppress("ComplexMethod", "NestedBlockDepth") internal fun ByteArray.binarySearch(labels: List, labelIndex: Int): String? { diff --git a/app/src/main/res/layout/decrypt_layout.xml b/app/src/main/res/layout/decrypt_layout.xml index 590661b3..664cb482 100644 --- a/app/src/main/res/layout/decrypt_layout.xml +++ b/app/src/main/res/layout/decrypt_layout.xml @@ -19,7 +19,7 @@ android:padding="16dp"> @@ -65,7 +65,7 @@ android:layout_marginTop="16dp" android:layout_marginBottom="16dp" android:src="@drawable/divider" - app:layout_constraintTop_toBottomOf="@id/crypto_password_last_changed" + app:layout_constraintTop_toBottomOf="@id/password_last_changed" tools:ignore="ContentDescription" /> - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/password_creation_activity.xml b/app/src/main/res/layout/password_creation_activity.xml new file mode 100644 index 00000000..13af597c --- /dev/null +++ b/app/src/main/res/layout/password_creation_activity.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/context_pass.xml b/app/src/main/res/menu/context_pass.xml index 3d262e14..41a1f705 100644 --- a/app/src/main/res/menu/context_pass.xml +++ b/app/src/main/res/menu/context_pass.xml @@ -14,12 +14,6 @@ android:title="@string/move" app:showAsAction="ifRoom" /> - - diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 0ab9c7f1..c76b435c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -22,7 +22,6 @@ رسالة مِن jgit: \n - هل نسيت إدخال إسم المستخدم ؟ خال من مفاتيح الـ SSH إستيراد توليد @@ -34,14 +33,9 @@ البروتوكول عنوان الخادوم 22 - 22 - 443 مسار المستودع - path/to/pass إسم المستخدم - git_username - عنوان الرابط الناتج نوع المصادقة إسم المستخدم @@ -87,7 +81,6 @@ مستودع تخزين خارجي إستخدم كلمة مرور المستودع الخارجي إختيار مستودع التخزين الخارجي - إستخدم أداة إختيار الملفات الإفتراضي تصدير كلمات السر النسخة @@ -105,7 +98,6 @@ تعليق توليد نسخ - إظهار العبارة السرية حسناً @@ -119,7 +111,6 @@ تحديث القائمة إظهار كلمة السر إظهار المزيد من المحتوى - عنوان المستودع أيقونة التطبيق أيقونة المجلد diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a6cde10c..24a1913d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -26,8 +26,8 @@ Toto přepíše%1$s %2$s . - Přídat generované heslo pro %1$s s použítím android password store. - Upravit heslo %1$s s použítím android password store. + Přídat generované heslo pro %1$s s použítím Android Password Store. + Upravit heslo %1$s s použítím Android Password Store. Odstranit %1$s ze store. Nebyl vybrán poskytovatel OpenPGP! @@ -43,7 +43,6 @@ Zpráva od jgit: \n - Zapomněli jste uvést přihlašovací jméno? Importujte nebo si prosím vygenerujte svůj SSH klíč v nastavení aplikace Žádný SSH klíč Import @@ -64,21 +63,13 @@ Protokol URL serveru 22 - 22 - 443 Cesta k repozitáři - cesta/k/heslům Jméno - git_username - Výsledná URL Mód ověření - Při použití vlastního portu, zadejte absolutní cestu (začíná "/") - Jméno Email - email Zadejte prosím platnou emailovou adresu Klonovat! @@ -154,13 +145,11 @@ Generovat Kopírovat Přidat tento veřejný klíč na Git server. - Zobrazit bezpečnostní frázi OK Ano Ne - Ajaj… Zrušit Synchronizovat repozitář Stáhnout ze serveru diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 77d3fb4f..aae2e888 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -29,7 +29,6 @@ Message from jgit: \n - Hast du vergessen einen Nutzernamen zu vergeben? Please import or generate your SSH key file in the preferences Kein SSH-Key angegeben Import @@ -45,18 +44,11 @@ Protokoll Server URL 22 - 22 - 443 Repo-Pfad - path/to/pass Nutzername - Git-Nutzername - Erzeugte URL Authentifizierungsmethode - Wenn du einen anderen Port nutzt, setze den absoluten Pfad (startet mit "/") - Nutzername Bitte valide Email eingeben Klone! @@ -112,7 +104,6 @@ Externes Repository Nutze ein externes Repository Wähle ein externes Repository - Benutze Standardauswahl für Dateien Passwörter exportieren Exportiert die verschlüsselten Passwörter in ein externes Verzeichnis Version @@ -132,13 +123,11 @@ Generieren Kopieren Füge den Public-Key zu deinem Git-Server hinzu. - Zeige Passwort OK Ja Nein - Oops… Abbruch Synchronisiere Repository Git Pull @@ -153,7 +142,6 @@ Passwort senden als Nur-Text mit behilfe von… Password wiedergeben Zeige weiteren Inhalt - Repository URI App Icon Verzeichnis Icon diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7ef91026..7a8309bb 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -38,7 +38,6 @@ Mensaje de jgit: \n - Olvidaste especificar un nombre de usuario? Por favor importa o genera tu llave SSH en los ajustes No hay llave SSH Importar @@ -61,18 +60,11 @@ Protocolo URL de servidor 22 - 22 - 443 Ruta del repositorio - ruta/a/claves Nombre de usuario - nombre_usuario - URL resultante Modo de autenticación - Al usar puertos personalizados, ingresa una ruta absoluta (empieza con "/") - Nombre de usuario Por favor ingresa una dirección de correo ¡Clonar! @@ -134,7 +126,6 @@ Repositorio externo Usar un repositorio externo para contraseñas Seleccionar repositorio externo - Usar seleccionador de archivos por defecto Exportar contraseñas Exporta las contraseñas cifradas a un directorio externo. Versión @@ -160,13 +151,11 @@ Generar Copiar Registra esta llave pública en tu servidor Git. - Mostrar contraseña OK No - Ups… Cancelar Sincronizar con servidor Descargar del servidor @@ -181,7 +170,6 @@ Enviar contraseña en texto plano usando… Mostrar contraseña Mostrar contenido extra - URI del repositorio Ícono de app Ícono de directorio @@ -212,6 +200,5 @@ Hackish tools Abortar rebase Hash del commit - Username: Nombre de usuario\n… o algún contenido extra Error al obtener la fecha de último cambio diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 32ee4bec..945e3559 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -44,7 +44,6 @@ Message de jgit: \n - Avez-vous oublié de renseigner votre nom d\'utilisateur ? Vous devez importer ou générer votre fichier de clef SSH dans les préférences Absence de clef SSH Importer @@ -67,21 +66,13 @@ Protocole URL du serveur 22 - 22 - 443 Chemin du dépôt - path/to/pass Nom d\'utilisateur - git_username - URL finale Méthode d\'authentification - Lors de l\'utilisation d\'un numéro de port personnalisé, fournissez un chemin absolu (commençant par "/") - Nom d\'utilisateur Email - email Merci de saisir une adresse mail valide Cloner ! @@ -139,7 +130,6 @@ Dépôt externe Utilise un dépôt externe pour les mots de passe Choisissez un dépôt externe - Utiliser le selecteur de fichier par défaut Exporter les mots de passe Exporter les mots de passe (chiffrés) vers un répertoire externe Version @@ -161,13 +151,11 @@ Générer Copier Enregistrez cette clef publique sur votre serveur Git. - Afficher le mot de passe OK Oui Non - Oups… Annuler Synchronisation du dépôt Importer du serveur @@ -182,7 +170,6 @@ Envoyer le mot de passe en clair via… Montrer le mot de passe Afficher le contenu supplémentaire - Adresse du dépot Icône de l\'application Icône du dossier @@ -212,6 +199,5 @@ Se rappeler de la phrase secrète dans la configuration de l\'application (peu sûr) Outils de hack Commettre la clé - nom d\'utilisateur: quelque chose d\'autre contenu supplémentaire Failed to get last changed date diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 82d99da6..8bdec1e7 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -30,7 +30,6 @@ jgit からのメッセージ: \n - ユーザー名の指定を忘れましたか? プリファレンスで SSH 鍵ファイルをインポートまたは生成してください SSH 鍵がありませんkey インポート @@ -46,18 +45,11 @@ プロトコル サーバー URL 22 - 22 - 443 リポジトリのパス - path/to/pass ユーザー名 - git_username - 結果 URL 認証モード - カスタムポートを使用する場合は、絶対パスを入力 ("/" で始まる) - ユーザー名 名前 @@ -114,13 +106,11 @@ 生成 コピー この公開鍵を Git サーバーに提供してください。 - パスフレーズを表示 OK はい いいえ - おっと… キャンセル リポジトリを同期 リモートからプル diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 53de0bed..7fcc054a 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -9,14 +9,10 @@ #FF373737 #FF000000 #FFFF7539 - #FFFFa667 - #FFC54506 #FFFFFFFF - #FFFFFFFF @color/primary_color - #FF111111 @color/primary_color #66EEEEEE @color/window_background diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 39d22662..836b5672 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -46,7 +46,6 @@ Сообщение от jgit: \n - Вы забыли указать имя пользователя? Пожалуйста, импортируйте или сгенерируйте новый SSH ключ в настройках Нет SSH ключа Импортировать @@ -69,21 +68,13 @@ Протокол URL сервера 22 - 22 - 443 Путь к репозиторию - путь/до/пароля Имя пользователя - git_username - Получившийся URL Тип авторизации - При использовании нестандартных портов, укажите полный путь (начинается с "/") - Имя пользователя Электронная почта - электронная почта Введите корректный email Клонировать @@ -149,7 +140,6 @@ Внешний репозиторий Использовать внешний репозиторий Выбрать внешний репозиторий - Использовать стандартное окно выбора файлов Экспортировать пароли Экспортировать пароли в открытом виде во внешнее хранилище Версия @@ -193,13 +183,11 @@ Сгенерировать Скоприровать Поместите публичный ключ на сервер Git - Показать пароль OK Да Нет - Упс… Отмена Синхронизировать репозиторий Пулл с удаленного сервера @@ -215,7 +203,6 @@ Показать пароль Показать дополнительную информацию Скрыть расширенный контекст - URI репозитория Иконка приложения Иконка папки @@ -226,9 +213,7 @@ Сохранение не удалось из-за внутренней ошибки Это приложение в настоящее время не поддерживается Пароли не совпадают - Невозможно извлечь пароль, пожалуйста, используйте другой браузер Сгенерировать пароль... - Издатель приложения изменился; это может быть попытка фишинга. Достигнуто максимальное количество совпадений (%1$d); очистите совпадения перед тем как добавите новые. Издатель приложения изменился с тех пор как вы первый раз связали с ним запись хранилища паролей: Установленное приложение может попытаться украсть ваши учетные данные, выдавая себя за доверенное приложение\n\nПопробуйте удалить или переустановить  приложение из доверенного источника, такого как Play Store, Amazon Appstore, F-Droid или магазин приложений производителя вашего смартфона. @@ -275,7 +260,6 @@ Прервать перебазирование и записать изменения в новую ветку Полный сброс до состояния удаленной ветки Хэш-сумма изменений - имя пользователя: какой-то другой дополнительный контент Failed to get last changed date Ошибка при подключении к сервису OpenKeychain SSH API Не найдено SSH API провайдеров. OpenKeychain установлен? diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 5c5ba140..ff3ad60a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -30,7 +30,6 @@ Message from jgit: - 你忘了提供用户名了吗? 请在设置中导入或生成你的SSH密钥文件 无SSH密钥 导入 @@ -46,18 +45,11 @@ 接口 服务器 URL 22 - 22 - 443 Repo 路径 - path/to/pass 用户名 - git_username - 生成的 URL 认证模式 - 如果使用自定义端口, 请提供绝对路径 (从根目录开始) - 用户名 名称 @@ -111,13 +103,11 @@ 生成 复制 在你的Git服务器上提供此公钥 - 显示口令 确定 确定 - 糟糕… 取消 同步 Repo Git Pull diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c9ce813d..cb35d387 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -27,7 +27,6 @@ Message from jgit: - 你忘記輸入使用者名稱了嗎? 請在設定中匯入或產生你的 SSH 金鑰 無 SSH 金鑰 匯入 @@ -43,18 +42,11 @@ port 伺服器 URL 22 - 22 - 443 Repo 路徑 - path/to/pass 使用者名稱 - git_username - 生成的 URL 認證模式 - 如果使用自定 port, 請使用绝對路徑 (從根目錄開始) - 使用者名稱 名稱 @@ -108,13 +100,11 @@ 產生 複製 在你的 Git 伺服器上提供此公鑰 - 顯示密碼 確定 確定 - 糟糕… 取消 同步 Repo Git Pull diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 2849dd4f..5ede6b6f 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -4,15 +4,6 @@ --> - - ssh-key - username/password - OpenKeychain - - - ssh:// - https:// - @string/pref_folder_first_sort_order @string/pref_file_first_sort_order @@ -23,12 +14,6 @@ FILE_FIRST INDEPENDENT - - 0 - 1 - 2 - 3 - lowercase UPPERCASE diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 04f1e99b..55136a37 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -9,17 +9,13 @@ #8eacbb #34515e #ff7043 - #ffa270 - #c63f17 #212121 - #ffffff #ffffffff #eceff1 #D4F1EA @color/primary_text_color - #FFFFFF #668eacbb #000000 @color/primary_dark_color diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 26d94ff7..a76d0bab 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -8,7 +8,6 @@ 16dp 16dp 16dp - 8dp 8dp 56dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e37b0eb..4574c44a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,8 +38,8 @@ This will overwrite %1$s with %2$s. - Add generated password for %1$s using android password store. - Edit password for %1$s using android password store. + Add generated password for %1$s using Android Password Store. + Edit password for %1$s using Android Password Store. Remove %1$s from store. Rename %1$s to %2$s. @@ -58,7 +58,6 @@ Message from jgit: \n - Did you forget to specify a username? Please fix the remote server configuration in settings before proceeding Please import or generate your SSH key file in the preferences No SSH key @@ -81,26 +80,15 @@ Server Protocol Server URL - server.com Port - 22 - 443 Repo path - path/to/pass Username - git_username - Resulting URL Authentication Mode - When using custom ports, provide an absolute path (starts with "/") - - Git config - Username Username Email - email Please enter a valid email address Clone @@ -172,7 +160,6 @@ External repository Use an external password repository Select external repository - Use default file picker Export passwords Exports the encrypted passwords to an external directory Version @@ -216,7 +203,6 @@ Generate Copy Provide this public key to your Git server. - Show passphrase Generating keys… Done! 2048 @@ -228,7 +214,6 @@ No Go to Settings Go back - Oops… Cancel Synchronize repository Pull from remote @@ -244,7 +229,6 @@ Show password Show extra content Hide extra content - Repository URI App icon Folder icon @@ -257,9 +241,7 @@ Save failed due to an internal error This app is currently not supported Passwords don\'t match - Couldn\'t extract password, please use a different browser for now Generate password… - The app\'s publisher has changed; this may be a phishing attempt. Maximum number of matches (%1$d) reached; clear matches before adding new ones. This app\'s publisher has changed since you first associated a Password Store entry with it: The currently installed app may be trying to steal your credentials by pretending to be a trusted app.\n\nTry to uninstall and reinstall the app from a trusted source, such as the Play Store, Amazon Appstore, F-Droid, or your phone manufacturer\'s store. @@ -313,7 +295,6 @@ Abort rebase and push new branch Hard reset to remote branch Commit hash - username: something other extra content Failed to get last changed date Failed to connect to OpenKeychain SSH API service. No SSH API provider found. Is OpenKeychain installed? @@ -376,4 +357,13 @@ Custom domains Autofill will distinguish subdomains of these domains company.com\npersonal.com + + + Incorrect passphrase + No matching PGP keys found + Error from OpenKeyChain : %s + + + Error + Failed to write password file to the store, please try again. -- cgit v1.2.3