diff options
Diffstat (limited to 'app/src/main')
41 files changed, 1065 insertions, 1160 deletions
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" /> + <activity + android:name=".crypto.PasswordCreationActivity" + android:label="@string/new_password_title" + android:parentActivityName=".PasswordStore" + android:windowSoftInputMode="adjustResize" /> + + <activity + android:name=".crypto.DecryptActivity" + android:parentActivityName=".PasswordStore" + android:windowSoftInputMode="adjustResize" /> + + <activity + android:name=".crypto.GetKeyIdsActivity" + android:parentActivityName=".PasswordStore" + android:theme="@style/NoBackgroundTheme" /> + <service android:name=".autofill.AutofillService" android:enabled="@bool/enable_accessibility_autofill" @@ -98,11 +114,6 @@ <activity android:name=".autofill.AutofillPreferenceActivity" /> - <activity - android:name=".crypto.PgpActivity" - android:configChanges="orientation|screenSize" - android:parentActivityName=".PasswordStore" - android:windowSoftInputMode="adjustResize" /> <activity android:name=".SelectFolderActivity" /> <activity android:name=".sshkeygen.SshKeyGenActivity" diff --git a/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt b/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt index 5349d6c3..ceb84020 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt @@ -8,17 +8,16 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service -import android.content.ClipboardManager +import android.content.ClipData import android.content.Intent import android.content.SharedPreferences import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import androidx.core.content.getSystemService -import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.preference.PreferenceManager import com.github.ajalt.timberkt.d -import com.zeapo.pwdstore.utils.ClipboardUtils +import com.zeapo.pwdstore.utils.clipboard import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -60,7 +59,6 @@ class ClipboardService : Service() { startTimer(time) } withContext(Dispatchers.Main) { - emitBroadcast() clearClipboard() stopForeground(true) stopSelf() @@ -85,11 +83,21 @@ class ClipboardService : Service() { private fun clearClipboard() { val deepClear = settings.getBoolean("clear_clipboard_20x", false) - val clipboardManager = getSystemService<ClipboardManager>() + val clipboard = clipboard - if (clipboardManager is ClipboardManager) { + if (clipboard != null) { scope.launch { - ClipboardUtils.clearClipboard(clipboardManager, deepClear) + d { "Clearing the clipboard" } + val clip = ClipData.newPlainText("pgp_handler_result_pm", "") + clipboard.setPrimaryClip(clip) + if (deepClear) { + withContext(Dispatchers.IO) { + repeat(20) { + val count = (it * 500).toString() + clipboard.setPrimaryClip(ClipData.newPlainText(count, count)) + } + } + } } } else { d { "Cannot get clipboard manager service" } @@ -105,12 +113,6 @@ class ClipboardService : Service() { } } - private fun emitBroadcast() { - val localBroadcastManager = LocalBroadcastManager.getInstance(this) - val clearIntent = Intent(ACTION_CLEAR) - localBroadcastManager.sendBroadcast(clearIntent) - } - private fun createNotification() { createNotificationChannel() val clearIntent = Intent(this, ClipboardService::class.java) @@ -151,7 +153,7 @@ class ClipboardService : Service() { companion object { private const val ACTION_CLEAR = "ACTION_CLEAR_CLIPBOARD" - private const val ACTION_START = "ACTION_START_CLIPBOARD_TIMER" + const val ACTION_START = "ACTION_START_CLIPBOARD_TIMER" private const val CHANNEL_ID = "NotificationService" } } diff --git a/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt b/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt index b5902199..b452f521 100644 --- a/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt @@ -10,7 +10,7 @@ import android.os.Handler import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit import androidx.preference.PreferenceManager -import com.zeapo.pwdstore.crypto.PgpActivity +import com.zeapo.pwdstore.crypto.DecryptActivity import com.zeapo.pwdstore.utils.BiometricAuthenticator class LaunchActivity : AppCompatActivity() { @@ -39,13 +39,12 @@ class LaunchActivity : AppCompatActivity() { } private fun startTargetActivity(noAuth: Boolean) { - if (intent?.getStringExtra("OPERATION") == "DECRYPT") { - val decryptIntent = Intent(this, PgpActivity::class.java) + if (intent.action == ACTION_DECRYPT_PASS) { + val decryptIntent = Intent(this, DecryptActivity::class.java) decryptIntent.putExtra("NAME", intent.getStringExtra("NAME")) decryptIntent.putExtra("FILE_PATH", intent.getStringExtra("FILE_PATH")) decryptIntent.putExtra("REPO_PATH", intent.getStringExtra("REPO_PATH")) decryptIntent.putExtra("LAST_CHANGED_TIMESTAMP", intent.getLongExtra("LAST_CHANGED_TIMESTAMP", 0L)) - decryptIntent.putExtra("OPERATION", "DECRYPT") startActivity(decryptIntent) } else { startActivity(Intent(this, PasswordStore::class.java)) @@ -53,4 +52,8 @@ class LaunchActivity : AppCompatActivity() { overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) Handler().postDelayed({ finish() }, if (noAuth) 0L else 500L) } + + companion object { + const val ACTION_DECRYPT_PASS = "DECRYPT_PASS" + } } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt index 03f2cfe1..24a1b7cb 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt @@ -155,11 +155,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) { // Called each time the action mode is shown. Always called after onCreateActionMode, but // may be called multiple times if the mode is invalidated. override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - menu.findItem(R.id.menu_edit_password).isVisible = - recyclerAdapter.getSelectedItems(requireContext()) - .map { it.type == PasswordItem.TYPE_PASSWORD } - .singleOrNull() == true - return true // Return false if nothing is done + return true } // Called when the user selects a contextual menu item @@ -174,13 +170,6 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) { mode.finish() // Action picked, so close the CAB return true } - R.id.menu_edit_password -> { - requireStore().editPassword( - recyclerAdapter.getSelectedItems(requireContext()).first() - ) - mode.finish() - return true - } R.id.menu_move_password -> { requireStore().movePasswords(recyclerAdapter.getSelectedItems(requireContext())) return false diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt index 00cfbf9a..ab9e3944 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt @@ -46,11 +46,10 @@ import com.google.android.material.snackbar.Snackbar import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel -import com.zeapo.pwdstore.crypto.PgpActivity -import com.zeapo.pwdstore.crypto.PgpActivity.Companion.getLongName +import com.zeapo.pwdstore.crypto.BasePgpActivity.Companion.getLongName +import com.zeapo.pwdstore.crypto.DecryptActivity +import com.zeapo.pwdstore.crypto.PasswordCreationActivity import com.zeapo.pwdstore.git.BaseGitActivity -import com.zeapo.pwdstore.git.GitAsyncTask -import com.zeapo.pwdstore.git.GitOperation import com.zeapo.pwdstore.git.GitOperationActivity import com.zeapo.pwdstore.git.GitServerConfigActivity import com.zeapo.pwdstore.git.config.ConnectionMode @@ -65,6 +64,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirect import com.zeapo.pwdstore.utils.PasswordRepository.Companion.initialize import com.zeapo.pwdstore.utils.PasswordRepository.Companion.isInitialized import com.zeapo.pwdstore.utils.PasswordRepository.PasswordSortOrder.Companion.getSortOrder +import com.zeapo.pwdstore.utils.commitChange import com.zeapo.pwdstore.utils.listFilesRecursively import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.errors.GitAPIException @@ -73,7 +73,7 @@ import java.io.File import java.lang.Character.UnicodeBlock import java.util.Stack -class PasswordStore : AppCompatActivity() { +class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { private lateinit var activity: PasswordStore private lateinit var searchItem: MenuItem @@ -123,7 +123,6 @@ class PasswordStore : AppCompatActivity() { savedInstance = null } super.onCreate(savedInstance) - setContentView(R.layout.activity_pwdstore) // If user is eligible for Oreo autofill, prompt them to switch. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && @@ -487,15 +486,16 @@ class PasswordStore : AppCompatActivity() { } fun decryptPassword(item: PasswordItem) { - val decryptIntent = Intent(this, PgpActivity::class.java) + val decryptIntent = Intent(this, DecryptActivity::class.java) val authDecryptIntent = Intent(this, LaunchActivity::class.java) for (intent in arrayOf(decryptIntent, authDecryptIntent)) { intent.putExtra("NAME", item.toString()) intent.putExtra("FILE_PATH", item.file.absolutePath) intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath) intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.file.absolutePath)) - intent.putExtra("OPERATION", "DECRYPT") } + // Needs an action to be a shortcut intent + authDecryptIntent.action = LaunchActivity.ACTION_DECRYPT_PASS // Adds shortcut if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { @@ -503,7 +503,7 @@ class PasswordStore : AppCompatActivity() { .setShortLabel(item.toString()) .setLongLabel(item.fullPathToParent + item.toString()) .setIcon(Icon.createWithResource(this, R.mipmap.ic_launcher)) - .setIntent(authDecryptIntent.setAction("DECRYPT_PASS")) // Needs action + .setIntent(authDecryptIntent) .build() val shortcuts = shortcutManager!!.dynamicShortcuts if (shortcuts.size >= shortcutManager!!.maxShortcutCountPerActivity && shortcuts.size > 0) { @@ -517,16 +517,6 @@ class PasswordStore : AppCompatActivity() { startActivityForResult(decryptIntent, REQUEST_CODE_DECRYPT_AND_VERIFY) } - fun editPassword(item: PasswordItem) { - val intent = Intent(this, PgpActivity::class.java) - intent.putExtra("NAME", item.toString()) - intent.putExtra("FILE_PATH", item.file.absolutePath) - intent.putExtra("PARENT_PATH", item.file.parentFile!!.absolutePath) - intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath) - intent.putExtra("OPERATION", "EDIT") - startActivityForResult(intent, REQUEST_CODE_EDIT) - } - private fun validateState(): Boolean { if (!isInitialized) { MaterialAlertDialogBuilder(this) @@ -553,10 +543,9 @@ class PasswordStore : AppCompatActivity() { if (!validateState()) return val currentDir = currentDir tag(TAG).i { "Adding file to : ${currentDir.absolutePath}" } - val intent = Intent(this, PgpActivity::class.java) + val intent = Intent(this, PasswordCreationActivity::class.java) intent.putExtra("FILE_PATH", currentDir.absolutePath) intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath) - intent.putExtra("OPERATION", "ENCRYPT") startActivityForResult(intent, REQUEST_CODE_ENCRYPT) } @@ -626,10 +615,6 @@ class PasswordStore : AppCompatActivity() { private val currentDir: File get() = plist?.currentDir ?: getRepositoryDirectory(applicationContext) - private fun commitChange(message: String) { - commitChange(this, message) - } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (resultCode == Activity.RESULT_OK) { when (requestCode) { @@ -650,11 +635,6 @@ class PasswordStore : AppCompatActivity() { data!!.extras!!.getString("LONG_NAME"))) refreshPasswordList() } - REQUEST_CODE_EDIT -> { - commitChange(resources.getString(R.string.git_commit_edit_text, - data!!.extras!!.getString("LONG_NAME"))) - refreshPasswordList() - } BaseGitActivity.REQUEST_INIT, NEW_REPO_BUTTON -> initializeRepositoryInfo() BaseGitActivity.REQUEST_SYNC, BaseGitActivity.REQUEST_PULL -> resetPasswordList() HOME -> checkLocalRepository() @@ -821,7 +801,6 @@ class PasswordStore : AppCompatActivity() { companion object { const val REQUEST_CODE_ENCRYPT = 9911 const val REQUEST_CODE_DECRYPT_AND_VERIFY = 9913 - const val REQUEST_CODE_EDIT = 9916 const val REQUEST_CODE_SELECT_FOLDER = 9917 const val REQUEST_ARG_PATH = "PATH" private val TAG = PasswordStore::class.java.name @@ -836,26 +815,5 @@ class PasswordStore : AppCompatActivity() { } private const val PREFERENCE_SEEN_AUTOFILL_ONBOARDING = "seen_autofill_onboarding" - - fun commitChange(activity: Activity, message: String, finishWithResultOnEnd: Intent? = null) { - if (!PasswordRepository.isGitRepo()) { - if (finishWithResultOnEnd != null) { - activity.setResult(Activity.RESULT_OK, finishWithResultOnEnd) - activity.finish() - } - return - } - object : GitOperation(getRepositoryDirectory(activity), activity) { - override fun execute() { - tag(TAG).d { "Committing with message $message" } - val git = Git(repository) - val tasks = GitAsyncTask(activity, true, this, finishWithResultOnEnd) - tasks.execute( - git.add().addFilepattern("."), - git.commit().setAll(true).setMessage(message) - ) - } - }.execute() - } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt b/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt index 4d8475df..568f86f3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt @@ -10,21 +10,16 @@ import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentManager +import androidx.fragment.app.commit import com.zeapo.pwdstore.utils.PasswordRepository -// TODO more work needed, this is just an extraction from PgpHandler -class SelectFolderActivity : AppCompatActivity() { +class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) { private lateinit var passwordList: SelectFolderFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.select_folder_layout) - - val fragmentManager = supportFragmentManager - val fragmentTransaction = fragmentManager.beginTransaction() - passwordList = SelectFolderFragment() val args = Bundle() args.putString(PasswordStore.REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory(applicationContext).absolutePath) @@ -33,10 +28,11 @@ class SelectFolderActivity : AppCompatActivity() { supportActionBar?.show() - fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - fragmentTransaction.replace(R.id.pgp_handler_linearlayout, passwordList, "PasswordsList") - fragmentTransaction.commit() + supportFragmentManager.commit { + replace(R.id.pgp_handler_linearlayout, passwordList, "PasswordsList") + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -45,8 +41,7 @@ class SelectFolderActivity : AppCompatActivity() { } override fun onOptionsItemSelected(item: MenuItem): Boolean { - val id = item.itemId - when (id) { + when (item.itemId) { android.R.id.home -> { setResult(Activity.RESULT_CANCELED) finish() diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 96f23724..04967c82 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -21,6 +21,7 @@ import android.text.TextUtils import android.view.MenuItem import android.view.accessibility.AccessibilityManager import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatTextView import androidx.biometric.BiometricManager @@ -42,7 +43,8 @@ import com.google.android.material.snackbar.Snackbar import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel -import com.zeapo.pwdstore.crypto.PgpActivity +import com.zeapo.pwdstore.crypto.BasePgpActivity +import com.zeapo.pwdstore.crypto.GetKeyIdsActivity import com.zeapo.pwdstore.git.GitConfigActivity import com.zeapo.pwdstore.git.GitServerConfigActivity import com.zeapo.pwdstore.pwgenxkpwd.XkpwdDictionary @@ -74,12 +76,13 @@ class UserPreference : AppCompatActivity() { private lateinit var autofillDependencies: List<Preference> private lateinit var oreoAutofillDependencies: List<Preference> private lateinit var callingActivity: UserPreference + private lateinit var sharedPreferences: SharedPreferences private lateinit var encryptedPreferences: SharedPreferences override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { callingActivity = requireActivity() as UserPreference val context = requireContext() - val sharedPreferences = preferenceManager.sharedPreferences + sharedPreferences = preferenceManager.sharedPreferences encryptedPreferences = requireActivity().applicationContext.getEncryptedPrefs("git_operation") addPreferencesFromResource(R.xml.preference) @@ -146,15 +149,6 @@ class UserPreference : AppCompatActivity() { viewSshKeyPreference?.isVisible = sharedPreferences.getBoolean("use_generated_key", false) deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean("git_external", false) clearClipboard20xPreference?.isVisible = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0 - val selectedKeys = (sharedPreferences.getStringSet("openpgp_key_ids_set", null) - ?: HashSet()).toTypedArray() - keyPreference?.summary = if (selectedKeys.isEmpty()) { - this.resources.getString(R.string.pref_no_key_selected) - } else { - selectedKeys.joinToString(separator = ";") { s -> - OpenPgpUtils.convertKeyIdToHex(java.lang.Long.valueOf(s)) - } - } openkeystoreIdPreference?.isVisible = sharedPreferences.getString("ssh_openkeystore_keyid", null)?.isNotEmpty() ?: false @@ -163,16 +157,21 @@ class UserPreference : AppCompatActivity() { appVersionPreference?.summary = "Version: ${BuildConfig.VERSION_NAME}" - keyPreference?.onPreferenceClickListener = ClickListener { - val providerPackageName = requireNotNull(sharedPreferences.getString("openpgp_provider_list", "")) - if (providerPackageName.isEmpty()) { - Snackbar.make(requireView(), resources.getString(R.string.provider_toast_text), Snackbar.LENGTH_LONG).show() - false - } else { - val intent = Intent(callingActivity, PgpActivity::class.java) - intent.putExtra("OPERATION", "GET_KEY_ID") - startActivityForResult(intent, IMPORT_PGP_KEY) - true + keyPreference?.let { pref -> + updateKeyIDsSummary(pref) + pref.onPreferenceClickListener = ClickListener { + val providerPackageName = requireNotNull(sharedPreferences.getString("openpgp_provider_list", "")) + if (providerPackageName.isEmpty()) { + Snackbar.make(requireView(), resources.getString(R.string.provider_toast_text), Snackbar.LENGTH_LONG).show() + false + } else { + val intent = Intent(callingActivity, GetKeyIdsActivity::class.java) + val keySelectResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + updateKeyIDsSummary(pref) + } + keySelectResult.launch(intent) + true + } } } @@ -366,13 +365,25 @@ class UserPreference : AppCompatActivity() { } } + private fun updateKeyIDsSummary(preference: Preference) { + val selectedKeys = (sharedPreferences.getStringSet("openpgp_key_ids_set", null) + ?: HashSet()).toTypedArray() + preference.summary = if (selectedKeys.isEmpty()) { + resources.getString(R.string.pref_no_key_selected) + } else { + selectedKeys.joinToString(separator = ";") { s -> + OpenPgpUtils.convertKeyIdToHex(s.toLong()) + } + } + } + private fun updateXkPasswdPrefsVisibility(newValue: Any?, prefIsCustomDict: CheckBoxPreference?, prefCustomDictPicker: Preference?) { when (newValue as String) { - PgpActivity.KEY_PWGEN_TYPE_CLASSIC -> { + BasePgpActivity.KEY_PWGEN_TYPE_CLASSIC -> { prefIsCustomDict?.isVisible = false prefCustomDictPicker?.isVisible = false } - PgpActivity.KEY_PWGEN_TYPE_XKPASSWD -> { + BasePgpActivity.KEY_PWGEN_TYPE_XKPASSWD -> { prefIsCustomDict?.isVisible = true prefCustomDictPicker?.isVisible = true } @@ -653,8 +664,6 @@ class UserPreference : AppCompatActivity() { .show() } } - EDIT_GIT_INFO -> { - } SELECT_GIT_DIRECTORY -> { val uri = data.data @@ -792,12 +801,10 @@ class UserPreference : AppCompatActivity() { companion object { private const val IMPORT_SSH_KEY = 1 - private const val IMPORT_PGP_KEY = 2 - private const val EDIT_GIT_INFO = 3 - private const val SELECT_GIT_DIRECTORY = 4 - private const val EXPORT_PASSWORDS = 5 - private const val EDIT_GIT_CONFIG = 6 - private const val SET_CUSTOM_XKPWD_DICT = 7 + private const val SELECT_GIT_DIRECTORY = 2 + private const val EXPORT_PASSWORDS = 3 + private const val EDIT_GIT_CONFIG = 4 + private const val SET_CUSTOM_XKPWD_DICT = 5 private const val TAG = "UserPreference" /** diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt index 3553d431..860d8459 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt @@ -16,31 +16,32 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.google.android.material.floatingactionbutton.FloatingActionButton import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.databinding.AutofillRecyclerViewBinding +import com.zeapo.pwdstore.utils.viewBinding import me.zhanghai.android.fastscroll.FastScrollerBuilder import java.lang.ref.WeakReference import java.util.ArrayList class AutofillPreferenceActivity : AppCompatActivity() { + private val binding by viewBinding(AutofillRecyclerViewBinding::inflate) internal var recyclerAdapter: AutofillRecyclerAdapter? = null // let fragment have access - private var recyclerView: RecyclerView? = null private var pm: PackageManager? = null private var recreate: Boolean = false // flag for action on up press; origin autofill dialog? different act public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - setContentView(R.layout.autofill_recycler_view) - recyclerView = findViewById(R.id.autofill_recycler) + setContentView(binding.root) val layoutManager = LinearLayoutManager(this) - recyclerView!!.layoutManager = layoutManager - recyclerView!!.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) - FastScrollerBuilder(recyclerView!!).build() + with(binding) { + autofillRecycler.layoutManager = layoutManager + autofillRecycler.addItemDecoration(DividerItemDecoration(this@AutofillPreferenceActivity, DividerItemDecoration.VERTICAL)) + FastScrollerBuilder(autofillRecycler).build() + } pm = packageManager @@ -105,7 +106,7 @@ class AutofillPreferenceActivity : AppCompatActivity() { companion object { private class PopulateTask(activity: AutofillPreferenceActivity) : AsyncTask<Void, Void, Void>() { - val weakReference = WeakReference<AutofillPreferenceActivity>(activity) + val weakReference = WeakReference(activity) override fun onPreExecute() { weakReference.get()?.apply { @@ -140,11 +141,13 @@ class AutofillPreferenceActivity : AppCompatActivity() { override fun onPostExecute(ignored: Void?) { weakReference.get()?.apply { runOnUiThread { - findViewById<View>(R.id.progress_bar).visibility = View.GONE - recyclerView!!.adapter = recyclerAdapter - val extras = intent.extras - if (extras != null) { - recyclerView!!.scrollToPosition(recyclerAdapter!!.getPosition(extras.getString("appName")!!)) + with(binding) { + progressBar.visibility = View.GONE + autofillRecycler.adapter = recyclerAdapter + val extras = intent.extras + if (extras != null) { + autofillRecycler.scrollToPosition(recyclerAdapter!!.getPosition(extras.getString("appName")!!)) + } } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt index 209892a7..fad13ec8 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt @@ -23,8 +23,9 @@ import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences import com.zeapo.pwdstore.autofill.oreo.Credentials import com.zeapo.pwdstore.autofill.oreo.FillableForm import com.zeapo.pwdstore.autofill.oreo.FormOrigin -import com.zeapo.pwdstore.crypto.PgpActivity +import com.zeapo.pwdstore.crypto.PasswordCreationActivity import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.commitChange import java.io.File @RequiresApi(Build.VERSION_CODES.O) @@ -97,15 +98,14 @@ class AutofillSaveActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val repo = PasswordRepository.getRepositoryDirectory(applicationContext) - val saveIntent = Intent(this, PgpActivity::class.java).apply { + val saveIntent = Intent(this, PasswordCreationActivity::class.java).apply { putExtras( bundleOf( "REPO_PATH" to repo.absolutePath, "FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath, - "OPERATION" to "ENCRYPT", - "SUGGESTED_NAME" to intent.getStringExtra(EXTRA_NAME), - "SUGGESTED_PASS" to intent.getStringExtra(EXTRA_PASSWORD), - "GENERATE_PASSWORD" to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) + PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME), + PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD), + PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) ) ) } @@ -144,10 +144,9 @@ class AutofillSaveActivity : Activity() { // Password was extracted from a form, there is nothing to fill. Intent() } - // PgpActivity delegates committing the added file to PasswordStore. Since PasswordStore - // is not involved in an AutofillScenario, we have to commit the file ourselves. - PasswordStore.commitChange( - this, + // PasswordCreationActivity delegates committing the added file to PasswordStore. Since + // PasswordStore is not involved in an AutofillScenario, we have to commit the file ourselves. + commitChange( getString(R.string.git_commit_add_text, longName), finishWithResultOnEnd = result ) diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt new file mode 100644 index 00000000..5206a15f --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt @@ -0,0 +1,270 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.crypto + +import android.app.PendingIntent +import android.content.ClipData +import android.content.Intent +import android.content.IntentSender +import android.content.SharedPreferences +import android.os.Build +import android.os.Bundle +import android.text.format.DateUtils +import android.view.WindowManager +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.annotation.CallSuper +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceManager +import com.github.ajalt.timberkt.Timber.tag +import com.github.ajalt.timberkt.e +import com.github.ajalt.timberkt.i +import com.google.android.material.snackbar.Snackbar +import com.zeapo.pwdstore.ClipboardService +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.UserPreference +import com.zeapo.pwdstore.utils.clipboard +import com.zeapo.pwdstore.utils.snackbar +import me.msfjarvis.openpgpktx.util.OpenPgpApi +import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection +import org.openintents.openpgp.IOpenPgpService2 +import org.openintents.openpgp.OpenPgpError +import java.io.File + +@Suppress("Registered") +open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { + + /** + * Full path to the repository + */ + val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") } + + /** + * Full path to the password file being worked on + */ + val fullPath: String by lazy { intent.getStringExtra("FILE_PATH") } + + /** + * Name of the password file + * + * Converts personal/auth.foo.org/john_doe@example.org.gpg to john_doe.example.org + */ + val name: String by lazy { File(fullPath).nameWithoutExtension } + + /** + * Get the timestamp for when this file was last modified. + */ + val lastChangedString: CharSequence by lazy { + getLastChangedString( + intent.getLongExtra( + "LAST_CHANGED_TIMESTAMP", + -1L + ) + ) + } + + /** + * [SharedPreferences] instance used by subclasses to persist settings + */ + val settings: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } + + /** + * Read-only field for getting the list of OpenPGP key IDs that we have access to. + */ + var keyIDs = emptySet<String>() + private set + + /** + * Handle to the [OpenPgpApi] instance that is used by subclasses to interface with OpenKeychain. + */ + private var serviceConnection: OpenPgpServiceConnection? = null + var api: OpenPgpApi? = null + + /** + * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots + * or recent apps screen and fills in [keyIDs] from [settings] + */ + @CallSuper + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + tag(TAG) + + keyIDs = settings.getStringSet("openpgp_key_ids_set", null) ?: emptySet() + } + + /** + * [onDestroy] handles unbinding from the OpenPgp service linked with [serviceConnection]. This + * is annotated with [CallSuper] because it's critical to unbind the service to ensure we're not + * leaking things. + */ + @CallSuper + override fun onDestroy() { + super.onDestroy() + serviceConnection?.unbindFromService() + } + + /** + * Sets up [api] once the service is bound. Downstream consumers must call super this to + * initialize [api] + */ + @CallSuper + override fun onBound(service: IOpenPgpService2) { + api = OpenPgpApi(this, service) + } + + /** + * Mandatory error handling from [OpenPgpServiceConnection.OnBound]. All subclasses must handle + * their own errors, and hence this class simply logs and rethrows. Subclasses Must NOT call super. + */ + override fun onError(e: Exception) { + e(e) { "Callers must handle their own exceptions" } + throw e + } + + /** + * Method for subclasses to initiate binding with [OpenPgpServiceConnection]. The design choices + * here are a bit dubious at first glance. We require passing a [ActivityResultLauncher] because + * it lets us react to having a OpenPgp provider selected without relying on the now deprecated + * [startActivityForResult]. + */ + fun bindToOpenKeychain(onBoundListener: OpenPgpServiceConnection.OnBound, activityResult: ActivityResultLauncher<Intent>) { + val providerPackageName = settings.getString("openpgp_provider_list", "") + if (providerPackageName.isNullOrEmpty()) { + Toast.makeText(this, resources.getString(R.string.provider_toast_text), Toast.LENGTH_LONG).show() + activityResult.launch(Intent(this, UserPreference::class.java)) + } else { + serviceConnection = OpenPgpServiceConnection(this, providerPackageName, onBoundListener) + serviceConnection?.bindToService() + } + } + + /** + * Handle the case where OpenKeychain returns that it needs to interact with the user + * + * @param result The intent returned by OpenKeychain + */ + fun getUserInteractionRequestIntent(result: Intent): IntentSender { + i { "RESULT_CODE_USER_INTERACTION_REQUIRED" } + return (result.getParcelableExtra(OpenPgpApi.RESULT_INTENT) as PendingIntent).intentSender + } + + /** + * Gets a relative string describing when this shape was last changed + * (e.g. "one hour ago") + */ + private fun getLastChangedString(timeStamp: Long): CharSequence { + if (timeStamp < 0) { + throw RuntimeException() + } + + return DateUtils.getRelativeTimeSpanString(this, timeStamp, true) + } + /** + * Base handling of OpenKeychain errors based on the error contained in [result]. Subclasses + * can use this when they want to default to sane error handling. + */ + fun handleError(result: Intent) { + val error: OpenPgpError? = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR) + if (error != null) { + when (error.errorId) { + OpenPgpError.NO_OR_WRONG_PASSPHRASE -> { + snackbar(message = getString(R.string.openpgp_error_wrong_passphrase)) + } + OpenPgpError.NO_USER_IDS -> { + snackbar(message = getString(R.string.openpgp_error_no_user_ids)) + } + else -> { + snackbar(message = getString(R.string.openpgp_error_unknown, error.message)) + e { "onError getErrorId: ${error.errorId}" } + e { "onError getMessage: ${error.message}" } + } + } + } + } + + /** + * Copies provided [text] to the clipboard. Shows a [Snackbar] which can be disabled by passing + * [showSnackbar] as false. + */ + fun copyTextToClipboard(text: String?, showSnackbar: Boolean = true) { + val clipboard = clipboard ?: return + val clip = ClipData.newPlainText("pgp_handler_result_pm", text) + clipboard.setPrimaryClip(clip) + if (showSnackbar) { + snackbar(message = resources.getString(R.string.clipboard_copied_text)) + } + } + + /** + * Copies a provided [password] string to the clipboard. This wraps [copyTextToClipboard] to + * hide the default [Snackbar] and starts off an instance of [ClipboardService] to provide a + * way of clearing the clipboard. + */ + fun copyPasswordToClipboard(password: String?) { + copyTextToClipboard(password, showSnackbar = false) + + var clearAfter = 45 + try { + clearAfter = (settings.getString("general_show_time", "45") ?: "45").toInt() + } catch (_: NumberFormatException) { + } + + if (clearAfter != 0) { + val service = Intent(this, ClipboardService::class.java).apply { + action = ClipboardService.ACTION_START + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(service) + } else { + startService(service) + } + snackbar(message = resources.getString(R.string.clipboard_password_toast_text, clearAfter)) + } else { + snackbar(message = resources.getString(R.string.clipboard_password_no_clear_toast_text)) + } + } + + companion object { + private const val TAG = "APS/BasePgpActivity" + const val KEY_PWGEN_TYPE_CLASSIC = "classic" + const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd" + + /** + * Gets the relative path to the repository + */ + fun getRelativePath(fullPath: String, repositoryPath: String): String = + fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") + + /** + * Gets the Parent path, relative to the repository + */ + fun getParentPath(fullPath: String, repositoryPath: String): String { + val relativePath = getRelativePath(fullPath, repositoryPath) + val index = relativePath.lastIndexOf("/") + return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace("/+".toRegex(), "/") + } + + /** + * /path/to/store/social/facebook.gpg -> social/facebook + */ + @JvmStatic + fun getLongName(fullPath: String, repositoryPath: String, basename: String): String { + var relativePath = getRelativePath(fullPath, repositoryPath) + return if (relativePath.isNotEmpty() && relativePath != "/") { + // remove preceding '/' + relativePath = relativePath.substring(1) + if (relativePath.endsWith('/')) { + relativePath + basename + } else { + "$relativePath/$basename" + } + } else { + basename + } + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt new file mode 100644 index 00000000..b7d7adcd --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt @@ -0,0 +1,202 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.crypto + +import android.content.Intent +import android.graphics.Typeface +import android.os.Bundle +import android.text.method.PasswordTransformationMethod +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult +import androidx.lifecycle.lifecycleScope +import com.github.ajalt.timberkt.e +import com.zeapo.pwdstore.PasswordEntry +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.databinding.DecryptLayoutBinding +import com.zeapo.pwdstore.utils.viewBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import me.msfjarvis.openpgpktx.util.OpenPgpApi +import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection +import org.openintents.openpgp.IOpenPgpService2 +import java.io.ByteArrayOutputStream +import java.io.File + +class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound { + private val binding by viewBinding(DecryptLayoutBinding::inflate) + + private val relativeParentPath by lazy { getParentPath(fullPath, repoPath) } + private var passwordEntry: PasswordEntry? = null + + private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result -> + if (result.data == null) { + setResult(RESULT_CANCELED, null) + finish() + return@registerForActivityResult + } + + when (result.resultCode) { + RESULT_OK -> decryptAndVerify(result.data) + RESULT_CANCELED -> { + setResult(RESULT_CANCELED, result.data) + finish() + } + } + } + + private val openKeychainResult = registerForActivityResult(StartActivityForResult()) { + decryptAndVerify() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bindToOpenKeychain(this, openKeychainResult) + title = name + with(binding) { + setContentView(root) + passwordCategory.text = relativeParentPath + passwordFile.text = name + passwordFile.setOnLongClickListener { + copyTextToClipboard(name) + true + } + try { + passwordLastChanged.text = resources.getString(R.string.last_changed, lastChangedString) + } catch (e: RuntimeException) { + passwordLastChanged.visibility = View.GONE + } + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.pgp_handler, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> onBackPressed() + R.id.edit_password -> editPassword() + R.id.share_password_as_plaintext -> shareAsPlaintext() + R.id.copy_password -> copyPasswordToClipboard(passwordEntry?.password) + } + return super.onOptionsItemSelected(item) + } + + override fun onBound(service: IOpenPgpService2) { + super.onBound(service) + decryptAndVerify() + } + + override fun onError(e: Exception) { + e(e) + } + + /** + * Edit the current password and hide all the fields populated by encrypted data so that when + * the result triggers they can be repopulated with new data. + */ + private fun editPassword() { + val intent = Intent(this, PasswordCreationActivity::class.java) + intent.putExtra("FILE_PATH", relativeParentPath) + intent.putExtra("REPO_PATH", repoPath) + intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name) + intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password) + intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContent) + intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true) + startActivity(intent) + finish() + } + + private fun shareAsPlaintext() { + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, passwordEntry?.password) + type = "text/plain" + } + // Always show a picker to give the user a chance to cancel + startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to))) + } + + private fun decryptAndVerify(receivedIntent: Intent? = null) { + if (api == null) { + bindToOpenKeychain(this, openKeychainResult) + return + } + val data = receivedIntent ?: Intent() + data.action = OpenPgpApi.ACTION_DECRYPT_VERIFY + + val inputStream = File(fullPath).inputStream() + val outputStream = ByteArrayOutputStream() + + lifecycleScope.launch(Dispatchers.IO) { + api?.executeApiAsync(data, inputStream, outputStream) { result -> + when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + OpenPgpApi.RESULT_CODE_SUCCESS -> { + try { + val showPassword = settings.getBoolean("show_password", true) + val showExtraContent = settings.getBoolean("show_extra_content", true) + val monoTypeface = Typeface.createFromAsset(assets, "fonts/sourcecodepro.ttf") + val entry = PasswordEntry(outputStream) + + passwordEntry = entry + + with(binding) { + if (entry.password.isEmpty()) { + passwordTextContainer.visibility = View.GONE + } else { + passwordTextContainer.visibility = View.VISIBLE + passwordText.typeface = monoTypeface + passwordText.setText(entry.password) + if (!showPassword) { + passwordText.transformationMethod = PasswordTransformationMethod.getInstance() + } + passwordTextContainer.setOnClickListener { copyPasswordToClipboard(entry.password) } + passwordText.setOnClickListener { copyPasswordToClipboard(entry.password) } + } + + if (entry.hasExtraContent()) { + extraContentContainer.visibility = View.VISIBLE + extraContent.typeface = monoTypeface + extraContent.setText(entry.extraContentWithoutUsername) + if (!showExtraContent) { + extraContent.transformationMethod = PasswordTransformationMethod.getInstance() + } + extraContentContainer.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) } + extraContent.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) } + + if (entry.hasUsername()) { + usernameText.typeface = monoTypeface + usernameText.setText(entry.username) + usernameTextContainer.setEndIconOnClickListener { copyTextToClipboard(entry.username) } + usernameTextContainer.visibility = View.VISIBLE + } else { + usernameTextContainer.visibility = View.GONE + } + } + } + + if (settings.getBoolean("copy_on_decrypt", true)) { + copyPasswordToClipboard(entry.password) + } + } catch (e: Exception) { + e(e) + } + } + OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { + val sender = getUserInteractionRequestIntent(result) + userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) + } + OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) + } + } + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt new file mode 100644 index 00000000..94d5b68c --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt @@ -0,0 +1,89 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.crypto + +import android.content.Intent +import android.os.Bundle +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult +import androidx.core.content.edit +import androidx.lifecycle.lifecycleScope +import com.github.ajalt.timberkt.Timber +import com.github.ajalt.timberkt.e +import com.zeapo.pwdstore.utils.snackbar +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import me.msfjarvis.openpgpktx.util.OpenPgpApi +import org.openintents.openpgp.IOpenPgpService2 + +class GetKeyIdsActivity : BasePgpActivity() { + + private val getKeyIds = registerForActivityResult(StartActivityForResult()) { getKeyIds() } + + private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result -> + if (result.data == null) { + setResult(RESULT_CANCELED, null) + finish() + return@registerForActivityResult + } + + when (result.resultCode) { + RESULT_OK -> getKeyIds(result.data) + RESULT_CANCELED -> { + setResult(RESULT_CANCELED, result.data) + finish() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bindToOpenKeychain(this, getKeyIds) + } + + override fun onBound(service: IOpenPgpService2) { + super.onBound(service) + getKeyIds() + } + + override fun onError(e: Exception) { + e(e) + } + + /** + * Get the Key ids from OpenKeychain + */ + private fun getKeyIds(receivedIntent: Intent? = null) { + val data = receivedIntent ?: Intent() + data.action = OpenPgpApi.ACTION_GET_KEY_IDS + lifecycleScope.launch(Dispatchers.IO) { + api?.executeApiAsync(data, null, null) { result -> + when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + OpenPgpApi.RESULT_CODE_SUCCESS -> { + try { + val ids = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS) + ?: LongArray(0) + val keys = ids.map { it.toString() }.toSet() + // use Long + settings.edit { putStringSet("openpgp_key_ids_set", keys) } + snackbar(message = "PGP keys selected") + setResult(RESULT_OK) + finish() + } catch (e: Exception) { + Timber.e(e) { "An Exception occurred" } + } + } + OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { + val sender = getUserInteractionRequestIntent(result) + userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) + } + OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) + } + } + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt new file mode 100644 index 00000000..7216a506 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt @@ -0,0 +1,296 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.crypto + +import android.content.Intent +import android.os.Bundle +import android.text.InputType +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.core.widget.doOnTextChanged +import androidx.lifecycle.lifecycleScope +import com.github.ajalt.timberkt.e +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zeapo.pwdstore.PasswordEntry +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences +import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure +import com.zeapo.pwdstore.databinding.PasswordCreationActivityBinding +import com.zeapo.pwdstore.ui.dialogs.PasswordGeneratorDialogFragment +import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment +import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.commitChange +import com.zeapo.pwdstore.utils.snackbar +import com.zeapo.pwdstore.utils.viewBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.eclipse.jgit.api.Git +import me.msfjarvis.openpgpktx.util.OpenPgpApi +import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException + +class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound { + + private val binding by viewBinding(PasswordCreationActivityBinding::inflate) + + private val suggestedName by lazy { intent.getStringExtra(EXTRA_FILE_NAME) } + private val suggestedPass by lazy { intent.getStringExtra(EXTRA_PASSWORD) } + private val suggestedExtra by lazy { intent.getStringExtra(EXTRA_EXTRA_CONTENT) } + private val shouldGeneratePassword by lazy { intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) } + private val doNothing = registerForActivityResult(StartActivityForResult()) {} + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bindToOpenKeychain(this, doNothing) + title = if (intent.getBooleanExtra(EXTRA_EDITING, false)) + getString(R.string.edit_password) + else + getString(R.string.new_password_title) + with(binding) { + setContentView(root) + generatePassword.setOnClickListener { generatePassword() } + + category.apply { + if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) { + isEnabled = true + } else { + setBackgroundColor(getColor(android.R.color.transparent)) + } + val path = getRelativePath(fullPath, repoPath) + // Keep empty path field visible if it is editable. + if (path.isEmpty() && !isEnabled) + visibility = View.GONE + else + setText(path) + } + suggestedName?.let { filename.setText(it) } + // Allow the user to quickly switch between storing the username as the filename or + // in the encrypted extras. This only makes sense if the directory structure is + // FileBased. + if (suggestedName == null && + AutofillPreferences.directoryStructure(this@PasswordCreationActivity) == + DirectoryStructure.FileBased + ) { + encryptUsername.apply { + visibility = View.VISIBLE + setOnClickListener { + if (isChecked) { + // User wants to enable username encryption, so we add it to the + // encrypted extras as the first line. + val username = filename.text.toString() + val extras = "username:$username\n${extraContent.text}" + + filename.setText("") + extraContent.setText(extras) + } else { + // User wants to disable username encryption, so we extract the + // username from the encrypted extras and use it as the filename. + val entry = PasswordEntry("PASSWORD\n${extraContent.text}") + val username = entry.username + + // username should not be null here by the logic in + // updateEncryptUsernameState, but it could still happen due to + // input lag. + if (username != null) { + filename.setText(username) + extraContent.setText(entry.extraContentWithoutUsername) + } + } + updateEncryptUsernameState() + } + } + listOf(filename, extraContent).forEach { + it.doOnTextChanged { _, _, _, _ -> updateEncryptUsernameState() } + } + } + suggestedPass?.let { + password.setText(it) + password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + suggestedExtra?.let { extraContent.setText(it) } + if (shouldGeneratePassword) { + generatePassword() + password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.pgp_handler_new_password, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home, R.id.cancel_password_add -> { + setResult(RESULT_CANCELED) + finish() + } + R.id.save_password -> encrypt() + R.id.save_and_copy_password -> encrypt(copy = true) + else -> return super.onOptionsItemSelected(item) + } + return true + } + + private fun generatePassword() { + when (settings.getString("pref_key_pwgen_type", KEY_PWGEN_TYPE_CLASSIC)) { + KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment() + .show(supportFragmentManager, "generator") + KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment() + .show(supportFragmentManager, "xkpwgenerator") + } + } + + private fun updateEncryptUsernameState() = with(binding) { + encryptUsername.apply { + if (visibility != View.VISIBLE) + return@with + val hasUsernameInFileName = filename.text.toString().isNotBlank() + // Use PasswordEntry to parse extras for username + val entry = PasswordEntry("PLACEHOLDER\n${extraContent.text}") + val hasUsernameInExtras = entry.hasUsername() + isEnabled = hasUsernameInFileName xor hasUsernameInExtras + isChecked = hasUsernameInExtras + } + } + + /** + * Encrypts the password and the extra content + */ + private fun encrypt(copy: Boolean = false) = with(binding) { + val editName = filename.text.toString().trim() + val editPass = password.text.toString() + val editExtra = extraContent.text.toString() + + if (editName.isEmpty()) { + snackbar(message = resources.getString(R.string.file_toast_text)) + return@with + } + + if (editPass.isEmpty() && editExtra.isEmpty()) { + snackbar(message = resources.getString(R.string.empty_toast_text)) + return@with + } + + if (copy) { + copyPasswordToClipboard(editPass) + } + + val data = Intent() + data.action = OpenPgpApi.ACTION_ENCRYPT + + // EXTRA_KEY_IDS requires long[] + val longKeys = keyIDs.map { it.toLong() } + data.putExtra(OpenPgpApi.EXTRA_KEY_IDS, longKeys.toLongArray()) + data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) + + val content = "$editPass\n$editExtra" + val inputStream = ByteArrayInputStream(content.toByteArray()) + val outputStream = ByteArrayOutputStream() + + val path = when { + // If we allowed the user to edit the relative path, we have to consider it here instead + // of fullPath. + category.isEnabled -> { + val editRelativePath = category.text.toString().trim() + if (editRelativePath.isEmpty()) { + snackbar(message = resources.getString(R.string.path_toast_text)) + return + } + val passwordDirectory = File("$repoPath/${editRelativePath.trim('/')}") + if (!passwordDirectory.exists() && !passwordDirectory.mkdir()) { + snackbar(message = "Failed to create directory ${editRelativePath.trim('/')}") + return + } + + "${passwordDirectory.path}/$editName.gpg" + } + else -> "$fullPath/$editName.gpg" + } + + lifecycleScope.launch(Dispatchers.IO) { + api?.executeApiAsync(data, inputStream, outputStream) { result -> + when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + OpenPgpApi.RESULT_CODE_SUCCESS -> { + try { + val file = File(path) + try { + file.outputStream().use { + it.write(outputStream.toByteArray()) + } + } catch (e: IOException) { + e(e) { "Failed to write password file" } + setResult(RESULT_CANCELED) + MaterialAlertDialogBuilder(this@PasswordCreationActivity) + .setTitle(getString(R.string.password_creation_file_write_fail_title)) + .setMessage(getString(R.string.password_creation_file_write_fail_message)) + .setCancelable(false) + .setPositiveButton(android.R.string.ok) { _, _ -> + finish() + } + .show() + } + + val returnIntent = Intent() + returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path) + returnIntent.putExtra(RETURN_EXTRA_NAME, editName) + returnIntent.putExtra(RETURN_EXTRA_LONG_NAME, getLongName(fullPath, repoPath, editName)) + + if (shouldGeneratePassword) { + val directoryStructure = + AutofillPreferences.directoryStructure(applicationContext) + val entry = PasswordEntry(content) + returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password) + val username = PasswordEntry(content).username + ?: directoryStructure.getUsernameFor(file) + returnIntent.putExtra(RETURN_EXTRA_USERNAME, username) + } + + val repo = PasswordRepository.getRepository(null) + if (repo != null) { + val status = Git(repo).status().call() + if (status.modified.isNotEmpty()) { + commitChange( + getString( + R.string.git_commit_edit_text, + getLongName(fullPath, repoPath, editName) + ) + ) + } + } + setResult(RESULT_OK, returnIntent) + finish() + } catch (e: Exception) { + e(e) { "An Exception occurred" } + } + } + OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) + } + } + } + } + + companion object { + private const val KEY_PWGEN_TYPE_CLASSIC = "classic" + private const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd" + const val RETURN_EXTRA_CREATED_FILE = "CREATED_FILE" + const val RETURN_EXTRA_NAME = "NAME" + const val RETURN_EXTRA_LONG_NAME = "LONG_NAME" + const val RETURN_EXTRA_USERNAME = "USERNAME" + const val RETURN_EXTRA_PASSWORD = "PASSWORD" + const val EXTRA_FILE_NAME = "FILENAME" + const val EXTRA_PASSWORD = "PASSWORD" + const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT" + const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD" + const val EXTRA_EDITING = "EDITING" + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt deleted file mode 100644 index 77d7e70c..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt +++ /dev/null @@ -1,788 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package com.zeapo.pwdstore.crypto - -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.IntentSender -import android.content.SharedPreferences -import android.graphics.Typeface -import android.os.Build -import android.os.Bundle -import android.text.InputType -import android.text.TextUtils -import android.text.format.DateUtils -import android.text.method.PasswordTransformationMethod -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.WindowManager -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.edit -import androidx.core.content.getSystemService -import androidx.core.widget.doOnTextChanged -import androidx.lifecycle.lifecycleScope -import androidx.localbroadcastmanager.content.LocalBroadcastManager -import androidx.preference.PreferenceManager -import com.github.ajalt.timberkt.Timber.e -import com.github.ajalt.timberkt.Timber.i -import com.github.ajalt.timberkt.Timber.tag -import com.google.android.material.snackbar.Snackbar -import com.zeapo.pwdstore.ClipboardService -import com.zeapo.pwdstore.PasswordEntry -import com.zeapo.pwdstore.R -import com.zeapo.pwdstore.UserPreference -import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences -import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure -import com.zeapo.pwdstore.ui.dialogs.PasswordGeneratorDialogFragment -import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment -import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_category_decrypt -import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_file -import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_last_changed -import kotlinx.android.synthetic.main.decrypt_layout.extra_content -import kotlinx.android.synthetic.main.decrypt_layout.extra_content_container -import kotlinx.android.synthetic.main.decrypt_layout.password_text -import kotlinx.android.synthetic.main.decrypt_layout.password_text_container -import kotlinx.android.synthetic.main.decrypt_layout.username_text -import kotlinx.android.synthetic.main.decrypt_layout.username_text_container -import kotlinx.android.synthetic.main.encrypt_layout.crypto_extra_edit -import kotlinx.android.synthetic.main.encrypt_layout.crypto_password_category -import kotlinx.android.synthetic.main.encrypt_layout.crypto_password_edit -import kotlinx.android.synthetic.main.encrypt_layout.crypto_password_file_edit -import kotlinx.android.synthetic.main.encrypt_layout.encrypt_username -import kotlinx.android.synthetic.main.encrypt_layout.generate_password -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.launch -import me.msfjarvis.openpgpktx.util.OpenPgpApi -import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.ACTION_DECRYPT_VERIFY -import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_CODE -import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_CODE_ERROR -import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_CODE_SUCCESS -import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_CODE_USER_INTERACTION_REQUIRED -import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_ERROR -import me.msfjarvis.openpgpktx.util.OpenPgpApi.Companion.RESULT_INTENT -import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection -import org.openintents.openpgp.IOpenPgpService2 -import org.openintents.openpgp.OpenPgpError -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.File -import java.nio.charset.Charset - -class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { - private val clipboard by lazy { getSystemService<ClipboardManager>() } - private var passwordEntry: PasswordEntry? = null - private var api: OpenPgpApi? = null - - private var editName: String? = null - private var editPass: String? = null - private var editExtra: String? = null - - private val suggestedName by lazy { intent.getStringExtra("SUGGESTED_NAME") } - private val suggestedPass by lazy { intent.getStringExtra("SUGGESTED_PASS") } - private val suggestedExtra by lazy { intent.getStringExtra("SUGGESTED_EXTRA") } - private val shouldGeneratePassword by lazy { intent.getBooleanExtra("GENERATE_PASSWORD", false) } - - private val operation: String by lazy { intent.getStringExtra("OPERATION") } - private val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") } - - private val fullPath: String by lazy { intent.getStringExtra("FILE_PATH") } - private val name: String by lazy { File(fullPath).nameWithoutExtension } - private val lastChangedString: CharSequence by lazy { - getLastChangedString( - intent.getLongExtra( - "LAST_CHANGED_TIMESTAMP", - -1L - ) - ) - } - private val relativeParentPath: String by lazy { getParentPath(fullPath, repoPath) } - - val settings: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } - private val keyIDs get() = _keyIDs - private var _keyIDs = emptySet<String>() - private var serviceConnection: OpenPgpServiceConnection? = null - private var delayTask: DelayShow? = null - private val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - delayTask?.doOnPostExecute() - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) - tag(TAG) - - // some persistence - _keyIDs = settings.getStringSet("openpgp_key_ids_set", null) ?: emptySet() - val providerPackageName = settings.getString("openpgp_provider_list", "") - - if (TextUtils.isEmpty(providerPackageName)) { - showSnackbar(resources.getString(R.string.provider_toast_text), Snackbar.LENGTH_LONG) - val intent = Intent(this, UserPreference::class.java) - startActivityForResult(intent, OPEN_PGP_BOUND) - } else { - // bind to service - serviceConnection = OpenPgpServiceConnection(this, providerPackageName, this) - serviceConnection?.bindToService() - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - when (operation) { - "DECRYPT", "EDIT" -> { - setContentView(R.layout.decrypt_layout) - crypto_password_category_decrypt.text = relativeParentPath - crypto_password_file.text = name - crypto_password_file.setOnLongClickListener { - val clipboard = clipboard ?: return@setOnLongClickListener false - val clip = ClipData.newPlainText("pgp_handler_result_pm", name) - clipboard.setPrimaryClip(clip) - showSnackbar(resources.getString(R.string.clipboard_copied_text)) - true - } - - crypto_password_last_changed.text = try { - resources.getString(R.string.last_changed, lastChangedString) - } catch (e: RuntimeException) { - showSnackbar(getString(R.string.get_last_changed_failed)) - "" - } - } - "ENCRYPT" -> { - setContentView(R.layout.encrypt_layout) - - generate_password?.setOnClickListener { - generatePassword() - } - - title = getString(R.string.new_password_title) - crypto_password_category.apply { - // If the activity has been provided with suggested info or is meant to generate - // a password, we allow the user to edit the path, otherwise we style the - // EditText like a TextView. - if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) { - isEnabled = true - } else { - setBackgroundColor(getColor(android.R.color.transparent)) - } - val path = getRelativePath(fullPath, repoPath) - // Keep empty path field visible if it is editable. - if (path.isEmpty() && !isEnabled) - visibility = View.GONE - else - setText(path) - } - suggestedName?.let { crypto_password_file_edit.setText(it) } - // Allow the user to quickly switch between storing the username as the filename or - // in the encrypted extras. This only makes sense if the directory structure is - // FileBased. - if (suggestedName != null && - AutofillPreferences.directoryStructure(this) == DirectoryStructure.FileBased - ) { - encrypt_username.apply { - visibility = View.VISIBLE - setOnClickListener { - if (isChecked) { - // User wants to enable username encryption, so we add it to the - // encrypted extras as the first line. - val username = crypto_password_file_edit.text!!.toString() - val extras = "username:$username\n${crypto_extra_edit.text!!}" - - crypto_password_file_edit.setText("") - crypto_extra_edit.setText(extras) - } else { - // User wants to disable username encryption, so we extract the - // username from the encrypted extras and use it as the filename. - val entry = PasswordEntry("PASSWORD\n${crypto_extra_edit.text!!}") - val username = entry.username - - // username should not be null here by the logic in - // updateEncryptUsernameState, but it could still happen due to - // input lag. - if (username != null) { - crypto_password_file_edit.setText(username) - crypto_extra_edit.setText(entry.extraContentWithoutUsername) - } - } - updateEncryptUsernameState() - } - } - crypto_password_file_edit.doOnTextChanged { _, _, _, _ -> updateEncryptUsernameState() } - crypto_extra_edit.doOnTextChanged { _, _, _, _ -> updateEncryptUsernameState() } - updateEncryptUsernameState() - } - suggestedPass?.let { - crypto_password_edit.setText(it) - crypto_password_edit.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - } - suggestedExtra?.let { crypto_extra_edit.setText(it) } - if (shouldGeneratePassword) { - generatePassword() - crypto_password_edit.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - } - } - } - } - - private fun updateEncryptUsernameState() { - encrypt_username.apply { - if (visibility != View.VISIBLE) - return - val hasUsernameInFileName = crypto_password_file_edit.text!!.toString().isNotBlank() - // Use PasswordEntry to parse extras for username - val entry = PasswordEntry("PLACEHOLDER\n${crypto_extra_edit.text!!}") - val hasUsernameInExtras = entry.hasUsername() - isEnabled = hasUsernameInFileName xor hasUsernameInExtras - isChecked = hasUsernameInExtras - } - } - - override fun onResume() { - super.onResume() - LocalBroadcastManager.getInstance(this).registerReceiver(receiver, IntentFilter(ACTION_CLEAR)) - } - - private fun generatePassword() { - when (settings.getString("pref_key_pwgen_type", KEY_PWGEN_TYPE_CLASSIC)) { - KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment() - .show(supportFragmentManager, "generator") - KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment() - .show(supportFragmentManager, "xkpwgenerator") - } - } - - override fun onStop() { - LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) - super.onStop() - } - - override fun onDestroy() { - super.onDestroy() - serviceConnection?.unbindFromService() - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - // Inflate the menu; this adds items to the action bar if it is present. - // Do not use the value `operation` in this case as it is not valid when editing - val menuId = when (intent.getStringExtra("OPERATION")) { - "ENCRYPT", "EDIT" -> R.menu.pgp_handler_new_password - "DECRYPT" -> R.menu.pgp_handler - else -> R.menu.pgp_handler - } - - menuInflater.inflate(menuId, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.crypto_cancel_add, android.R.id.home -> finish() - R.id.copy_password -> copyPasswordToClipBoard() - R.id.share_password_as_plaintext -> shareAsPlaintext() - R.id.edit_password -> editPassword() - R.id.crypto_confirm_add -> encrypt() - R.id.crypto_confirm_add_and_copy -> encrypt(true) - else -> return super.onOptionsItemSelected(item) - } - return true - } - - /** - * Shows a simple toast message - */ - private fun showSnackbar(message: String, length: Int = Snackbar.LENGTH_SHORT) { - runOnUiThread { Snackbar.make(findViewById(android.R.id.content), message, length).show() } - } - - /** - * Handle the case where OpenKeychain returns that it needs to interact with the user - * - * @param result The intent returned by OpenKeychain - * @param requestCode The code we'd like to use to identify the behaviour - */ - private fun handleUserInteractionRequest(result: Intent, requestCode: Int) { - i { "RESULT_CODE_USER_INTERACTION_REQUIRED" } - - val pi: PendingIntent? = result.getParcelableExtra(RESULT_INTENT) - try { - this@PgpActivity.startIntentSenderFromChild( - this@PgpActivity, pi?.intentSender, requestCode, - null, 0, 0, 0 - ) - } catch (e: IntentSender.SendIntentException) { - e(e) { "SendIntentException" } - } - } - - /** - * Handle the error returned by OpenKeychain - * - * @param result The intent returned by OpenKeychain - */ - private fun handleError(result: Intent) { - // TODO show what kind of error it is - /* For example: - * No suitable key found -> no key in OpenKeyChain - * - * Check in open-pgp-lib how their definitions and error code - */ - val error: OpenPgpError? = result.getParcelableExtra(RESULT_ERROR) - if (error != null) { - showSnackbar("Error from OpenKeyChain : " + error.message) - e { "onError getErrorId: ${error.errorId}" } - e { "onError getMessage: ${error.message}" } - } - } - - private fun initOpenPgpApi() { - api = api ?: OpenPgpApi(this, serviceConnection!!.service!!) - } - - private fun decryptAndVerify(receivedIntent: Intent? = null) { - val data = receivedIntent ?: Intent() - data.action = ACTION_DECRYPT_VERIFY - - val iStream = File(fullPath).inputStream() - val oStream = ByteArrayOutputStream() - - lifecycleScope.launch(IO) { - api?.executeApiAsync(data, iStream, oStream) { result -> - when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) { - RESULT_CODE_SUCCESS -> { - try { - val showPassword = settings.getBoolean("show_password", true) - val showExtraContent = settings.getBoolean("show_extra_content", true) - - password_text_container.visibility = View.VISIBLE - - val monoTypeface = Typeface.createFromAsset(assets, "fonts/sourcecodepro.ttf") - val entry = PasswordEntry(oStream) - - passwordEntry = entry - - if (intent.getStringExtra("OPERATION") == "EDIT") { - editPassword() - return@executeApiAsync - } - - if (entry.password.isEmpty()) { - password_text_container.visibility = View.GONE - } else { - password_text_container.visibility = View.VISIBLE - password_text.setText(entry.password) - if (!showPassword) { - password_text.transformationMethod = PasswordTransformationMethod.getInstance() - } - password_text_container.setOnClickListener { copyPasswordToClipBoard() } - password_text.setOnClickListener { copyPasswordToClipBoard() } - } - - if (entry.hasExtraContent()) { - extra_content_container.visibility = View.VISIBLE - extra_content.typeface = monoTypeface - extra_content.setText(entry.extraContentWithoutUsername) - if (!showExtraContent) { - extra_content.transformationMethod = PasswordTransformationMethod.getInstance() - } - extra_content_container.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) } - extra_content.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) } - - if (entry.hasUsername()) { - username_text.typeface = monoTypeface - username_text.setText(entry.username) - username_text_container.setEndIconOnClickListener { copyTextToClipboard(entry.username!!) } - username_text_container.visibility = View.VISIBLE - } else { - username_text_container.visibility = View.GONE - } - } - - if (settings.getBoolean("copy_on_decrypt", true)) { - copyPasswordToClipBoard() - } - } catch (e: Exception) { - e(e) { "An Exception occurred" } - } - } - RESULT_CODE_USER_INTERACTION_REQUIRED -> handleUserInteractionRequest(result, REQUEST_DECRYPT) - RESULT_CODE_ERROR -> handleError(result) - } - } - } - } - - /** - * Encrypts the password and the extra content - */ - private fun encrypt(copy: Boolean = false) { - editName = crypto_password_file_edit.text.toString().trim() - editPass = crypto_password_edit.text.toString() - editExtra = crypto_extra_edit.text.toString() - - if (editName?.isEmpty() == true) { - showSnackbar(resources.getString(R.string.file_toast_text)) - return - } - - if (editPass?.isEmpty() == true && editExtra?.isEmpty() == true) { - showSnackbar(resources.getString(R.string.empty_toast_text)) - return - } - - if (copy) { - copyPasswordToClipBoard() - } - - val data = Intent() - data.action = OpenPgpApi.ACTION_ENCRYPT - - // EXTRA_KEY_IDS requires long[] - val longKeys = keyIDs.map { it.toLong() } - data.putExtra(OpenPgpApi.EXTRA_KEY_IDS, longKeys.toLongArray()) - data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) - - // TODO Check if we could use PasswordEntry to generate the file - val content = "$editPass\n$editExtra" - val iStream = ByteArrayInputStream(content.toByteArray(Charset.forName("UTF-8"))) - val oStream = ByteArrayOutputStream() - - val path = when { - intent.getBooleanExtra("fromDecrypt", false) -> fullPath - // If we allowed the user to edit the relative path, we have to consider it here instead - // of fullPath. - crypto_password_category.isEnabled -> { - val editRelativePath = crypto_password_category.text!!.toString().trim() - if (editRelativePath.isEmpty()) { - showSnackbar(resources.getString(R.string.path_toast_text)) - return - } - "$repoPath/${editRelativePath.trim('/')}/$editName.gpg" - } - else -> "$fullPath/$editName.gpg" - } - - lifecycleScope.launch(IO) { - api?.executeApiAsync(data, iStream, oStream) { result -> - when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) { - RESULT_CODE_SUCCESS -> { - try { - // TODO This might fail, we should check that the write is successful - val file = File(path) - val outputStream = file.outputStream() - outputStream.write(oStream.toByteArray()) - outputStream.close() - - val returnIntent = Intent() - returnIntent.putExtra("CREATED_FILE", path) - returnIntent.putExtra("NAME", editName) - returnIntent.putExtra("LONG_NAME", getLongName(fullPath, repoPath, editName!!)) - - // if coming from decrypt screen->edit button - if (intent.getBooleanExtra("fromDecrypt", false)) { - returnIntent.putExtra("OPERATION", "EDIT") - returnIntent.putExtra("needCommit", true) - } - - if (shouldGeneratePassword) { - val directoryStructure = - AutofillPreferences.directoryStructure(applicationContext) - val entry = PasswordEntry(content) - returnIntent.putExtra("PASSWORD", entry.password) - val username = PasswordEntry(content).username - ?: directoryStructure.getUsernameFor(file) - returnIntent.putExtra("USERNAME", username) - } - - setResult(RESULT_OK, returnIntent) - finish() - } catch (e: Exception) { - e(e) { "An Exception occurred" } - } - } - RESULT_CODE_ERROR -> handleError(result) - } - } - } - } - - /** - * Opens EncryptActivity with the information for this file to be edited - */ - private fun editPassword() { - setContentView(R.layout.encrypt_layout) - generate_password?.setOnClickListener { - when (settings.getString("pref_key_pwgen_type", KEY_PWGEN_TYPE_CLASSIC)) { - KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment() - .show(supportFragmentManager, "generator") - KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment() - .show(supportFragmentManager, "xkpwgenerator") - } - } - - title = getString(R.string.edit_password_title) - - val monoTypeface = Typeface.createFromAsset(assets, "fonts/sourcecodepro.ttf") - crypto_password_edit.setText(passwordEntry?.password) - crypto_password_edit.typeface = monoTypeface - crypto_extra_edit.setText(passwordEntry?.extraContent) - crypto_extra_edit.typeface = monoTypeface - - crypto_password_category.setText(relativeParentPath) - crypto_password_file_edit.setText(name) - crypto_password_file_edit.isEnabled = false - - delayTask?.cancelAndSignal(true) - - val data = Intent(this, PgpActivity::class.java) - data.putExtra("OPERATION", "EDIT") - data.putExtra("fromDecrypt", true) - intent = data - invalidateOptionsMenu() - } - - /** - * Get the Key ids from OpenKeychain - */ - private fun getKeyIds(receivedIntent: Intent? = null) { - val data = receivedIntent ?: Intent() - data.action = OpenPgpApi.ACTION_GET_KEY_IDS - lifecycleScope.launch(IO) { - api?.executeApiAsync(data, null, null) { result -> - when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) { - RESULT_CODE_SUCCESS -> { - try { - val ids = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS) - ?: LongArray(0) - val keys = ids.map { it.toString() }.toSet() - - // use Long - settings.edit { putStringSet("openpgp_key_ids_set", keys) } - - showSnackbar("PGP keys selected") - - setResult(RESULT_OK) - finish() - } catch (e: Exception) { - e(e) { "An Exception occurred" } - } - } - RESULT_CODE_USER_INTERACTION_REQUIRED -> handleUserInteractionRequest(result, REQUEST_KEY_ID) - RESULT_CODE_ERROR -> handleError(result) - } - } - } - } - - override fun onError(e: Exception) {} - - /** - * The action to take when the PGP service is bound - */ - override fun onBound(service: IOpenPgpService2) { - initOpenPgpApi() - when (operation) { - "EDIT", "DECRYPT" -> decryptAndVerify() - "GET_KEY_ID" -> getKeyIds() - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - if (data == null) { - setResult(RESULT_CANCELED, null) - finish() - return - } - - // try again after user interaction - if (resultCode == RESULT_OK) { - when (requestCode) { - REQUEST_DECRYPT -> decryptAndVerify(data) - REQUEST_KEY_ID -> getKeyIds(data) - else -> { - setResult(RESULT_OK) - finish() - } - } - } else if (resultCode == RESULT_CANCELED) { - setResult(RESULT_CANCELED, data) - finish() - } - } - - private fun copyPasswordToClipBoard() { - val clipboard = clipboard ?: return - val pass = passwordEntry?.password - val clip = ClipData.newPlainText("pgp_handler_result_pm", pass) - clipboard.setPrimaryClip(clip) - - var clearAfter = 45 - try { - clearAfter = Integer.parseInt(settings.getString("general_show_time", "45") as String) - } catch (e: NumberFormatException) { - // ignore and keep default - } - - if (clearAfter != 0) { - setTimer() - showSnackbar(resources.getString(R.string.clipboard_password_toast_text, clearAfter)) - } else { - showSnackbar(resources.getString(R.string.clipboard_password_no_clear_toast_text)) - } - } - - private fun copyTextToClipboard(text: String) { - val clipboard = clipboard ?: return - val clip = ClipData.newPlainText("pgp_handler_result_pm", text) - clipboard.setPrimaryClip(clip) - showSnackbar(resources.getString(R.string.clipboard_copied_text)) - } - - private fun shareAsPlaintext() { - val sendIntent = Intent() - sendIntent.action = Intent.ACTION_SEND - sendIntent.putExtra(Intent.EXTRA_TEXT, passwordEntry?.password) - sendIntent.type = "text/plain" - startActivity( - Intent.createChooser( - sendIntent, - resources.getText(R.string.send_plaintext_password_to) - ) - ) // Always show a picker to give the user a chance to cancel - } - - private fun setTimer() { - - // make sure to cancel any running tasks as soon as possible - // if the previous task is still running, do not ask it to clear the password - delayTask?.cancelAndSignal(true) - - // launch a new one - delayTask = DelayShow() - delayTask?.execute() - } - - /** - * Gets a relative string describing when this shape was last changed - * (e.g. "one hour ago") - */ - private fun getLastChangedString(timeStamp: Long): CharSequence { - if (timeStamp < 0) { - throw RuntimeException() - } - - return DateUtils.getRelativeTimeSpanString(this, timeStamp, true) - } - - @Suppress("StaticFieldLeak") - inner class DelayShow { - - private var skip = false - private var service: Intent? = null - private var showTime: Int = 0 - - // Custom cancellation that can be triggered from another thread. - // - // This signals the DelayShow task to stop and avoids it having - // to poll the AsyncTask.isCancelled() excessively. If skipClearing - // is true, the cancelled task won't clear the clipboard. - fun cancelAndSignal(skipClearing: Boolean) { - skip = skipClearing - if (service != null) { - stopService(service) - service = null - } - } - - fun execute() { - service = Intent(this@PgpActivity, ClipboardService::class.java).also { - it.action = ACTION_START - } - doOnPreExecute() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(service) - } else { - startService(service) - } - } - - private fun doOnPreExecute() { - showTime = try { - Integer.parseInt(settings.getString("general_show_time", "45") as String) - } catch (e: NumberFormatException) { - 45 - } - password_text_container?.visibility = View.VISIBLE - if (extra_content?.text?.isNotEmpty() == true) - extra_content_container?.visibility = View.VISIBLE - } - - fun doOnPostExecute() { - if (skip) return - - if (password_text != null) { - passwordEntry = null - extra_content_container.visibility = View.INVISIBLE - password_text_container.visibility = View.INVISIBLE - finish() - } - } - } - - companion object { - const val OPEN_PGP_BOUND = 101 - const val REQUEST_DECRYPT = 202 - const val REQUEST_KEY_ID = 203 - - private const val ACTION_CLEAR = "ACTION_CLEAR_CLIPBOARD" - private const val ACTION_START = "ACTION_START_CLIPBOARD_TIMER" - - const val TAG = "PgpActivity" - - const val KEY_PWGEN_TYPE_CLASSIC = "classic" - const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd" - - /** - * Gets the relative path to the repository - */ - fun getRelativePath(fullPath: String, repositoryPath: String): String = - fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") - - /** - * Gets the Parent path, relative to the repository - */ - fun getParentPath(fullPath: String, repositoryPath: String): String { - val relativePath = getRelativePath(fullPath, repositoryPath) - val index = relativePath.lastIndexOf("/") - return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace("/+".toRegex(), "/") - } - - /** - * /path/to/store/social/facebook.gpg -> social/facebook - */ - @JvmStatic - fun getLongName(fullPath: String, repositoryPath: String, basename: String): String { - var relativePath = getRelativePath(fullPath, repositoryPath) - return if (relativePath.isNotEmpty() && relativePath != "/") { - // remove preceding '/' - relativePath = relativePath.substring(1) - if (relativePath.endsWith('/')) { - relativePath + basename - } else { - "$relativePath/$basename" - } - } else { - basename - } - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitAsyncTask.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitAsyncTask.kt index 124f2d0a..a2b4e2a8 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitAsyncTask.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitAsyncTask.kt @@ -32,7 +32,9 @@ class GitAsyncTask( activity: Activity, private val refreshListOnEnd: Boolean, private val operation: GitOperation, - private val finishWithResultOnEnd: Intent?) : AsyncTask<GitCommand<*>, Int, GitAsyncTask.Result>() { + private val finishWithResultOnEnd: Intent?, + private val silentlyExecute: Boolean = false +) : AsyncTask<GitCommand<*>, Int, GitAsyncTask.Result>() { private val activityWeakReference: WeakReference<Activity> = WeakReference(activity) private val activity: Activity? @@ -46,6 +48,7 @@ class GitAsyncTask( } override fun onPreExecute() { + if (silentlyExecute) return dialog.run { setMessage(activity!!.resources.getString(R.string.running_dialog_text)) setCancelable(false) @@ -141,7 +144,7 @@ class GitAsyncTask( } override fun onPostExecute(maybeResult: Result?) { - dialog.dismiss() + if (!silentlyExecute) dialog.dismiss() when (val result = maybeResult ?: Result.Err(IOException("Unexpected error"))) { is Result.Err -> { if (isExplicitlyUserInitiatedError(result.err)) { diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt index 1eac569b..1f5494fd 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt @@ -58,7 +58,7 @@ class GitServerConfigActivity : BaseGitActivity() { ConnectionMode.OpenKeychain -> check(R.id.connection_mode_open_keychain) ConnectionMode.None -> uncheck(checkedButtonId) } - addOnButtonCheckedListener { group, _, _ -> + addOnButtonCheckedListener { _, _, _ -> when (checkedButtonId) { R.id.connection_mode_ssh_key -> connectionMode = ConnectionMode.SshKey R.id.connection_mode_open_keychain -> connectionMode = ConnectionMode.OpenKeychain diff --git a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt index 5d35123d..8f4fbf84 100644 --- a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt @@ -7,15 +7,14 @@ package com.zeapo.pwdstore.sshkeygen import android.annotation.SuppressLint import android.app.Dialog import android.content.ClipData -import android.content.ClipboardManager import android.os.Bundle import android.view.View import android.widget.TextView import androidx.appcompat.app.AlertDialog -import androidx.core.content.getSystemService import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.utils.clipboard import java.io.File class ShowSshKeyFragment : DialogFragment() { @@ -39,8 +38,7 @@ class ShowSshKeyFragment : DialogFragment() { ad.setOnShowListener { val b = ad.getButton(AlertDialog.BUTTON_NEUTRAL) b.setOnClickListener { - val clipboard = activity.getSystemService<ClipboardManager>() - ?: return@setOnClickListener + val clipboard = activity.clipboard ?: return@setOnClickListener val clip = ClipData.newPlainText("public key", publicKey.text.toString()) clipboard.setPrimaryClip(clip) } diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt index 659c3f54..8f452dca 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt @@ -46,7 +46,7 @@ class PasswordGeneratorDialogFragment : DialogFragment() { val passwordText: AppCompatTextView = view.findViewById(R.id.passwordText) passwordText.typeface = monoTypeface builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> - val edit = callingActivity.findViewById<EditText>(R.id.crypto_password_edit) + val edit = callingActivity.findViewById<EditText>(R.id.password) edit.setText(passwordText.text) } builder.setNeutralButton(resources.getString(R.string.dialog_cancel)) { _, _ -> } diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt index 825a8890..52bc52c1 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt @@ -92,7 +92,7 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() { builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> setPreferences() - val edit = callingActivity.findViewById<EditText>(R.id.crypto_password_edit) + val edit = callingActivity.findViewById<EditText>(R.id.password) edit.setText(passwordText.text) } diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/ClipboardUtils.kt b/app/src/main/java/com/zeapo/pwdstore/utils/ClipboardUtils.kt deleted file mode 100644 index 2e408bfd..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/ClipboardUtils.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package com.zeapo.pwdstore.utils - -import android.content.ClipData -import android.content.ClipboardManager -import com.github.ajalt.timberkt.d -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -object ClipboardUtils { - - suspend fun clearClipboard(clipboard: ClipboardManager, deepClear: Boolean = false) { - d { "Clearing the clipboard" } - val clip = ClipData.newPlainText("pgp_handler_result_pm", "") - clipboard.setPrimaryClip(clip) - if (deepClear) { - withContext(Dispatchers.IO) { - repeat(20) { - val count = (it * 500).toString() - clipboard.setPrimaryClip(ClipData.newPlainText(count, count)) - } - } - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index d10bdaab..ff108b30 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -4,7 +4,10 @@ */ package com.zeapo.pwdstore.utils +import android.app.Activity +import android.content.ClipboardManager import android.content.Context +import android.content.Intent import android.content.SharedPreferences import android.os.Build import android.util.TypedValue @@ -16,7 +19,13 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.core.content.getSystemService import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKeys +import androidx.security.crypto.MasterKey +import com.github.ajalt.timberkt.d +import com.google.android.material.snackbar.Snackbar +import com.zeapo.pwdstore.git.GitAsyncTask +import com.zeapo.pwdstore.git.GitOperation +import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory +import org.eclipse.jgit.api.Git import java.io.File infix fun Int.hasFlag(flag: Int): Boolean { @@ -33,6 +42,16 @@ fun CharArray.clear() { } } +val Context.clipboard get() = getSystemService<ClipboardManager>() + +fun Activity.snackbar( + view: View = findViewById(android.R.id.content), + message: String, + length: Int = Snackbar.LENGTH_SHORT +) { + Snackbar.make(view, message, length).show() +} + fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList() fun Context.resolveAttribute(attr: Int): Int { @@ -42,17 +61,39 @@ fun Context.resolveAttribute(attr: Int): Int { } fun Context.getEncryptedPrefs(fileName: String): SharedPreferences { - val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC - val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec) + val masterKeyAlias = MasterKey.Builder(applicationContext) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() return EncryptedSharedPreferences.create( + applicationContext, fileName, masterKeyAlias, - this, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) } +fun Activity.commitChange(message: String, finishWithResultOnEnd: Intent? = null) { + if (!PasswordRepository.isGitRepo()) { + if (finishWithResultOnEnd != null) { + setResult(Activity.RESULT_OK, finishWithResultOnEnd) + finish() + } + return + } + object : GitOperation(getRepositoryDirectory(this@commitChange), this@commitChange) { + override fun execute() { + d { "Comitting with message: '$message'" } + val git = Git(repository) + val task = GitAsyncTask(this@commitChange, true, this, finishWithResultOnEnd, silentlyExecute = true) + task.execute( + git.add().addFilepattern("."), + git.commit().setAll(true).setMessage(message) + ) + } + }.execute() +} + /** * Extension function for [AlertDialog] that requests focus for the * view whose id is [id]. Solution based on a StackOverflow diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt index 903f6402..5ca95d31 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt @@ -4,7 +4,7 @@ */ package com.zeapo.pwdstore.utils -import com.zeapo.pwdstore.crypto.PgpActivity +import com.zeapo.pwdstore.crypto.BasePgpActivity import java.io.File data class PasswordItem( @@ -19,7 +19,7 @@ data class PasswordItem( .replace(rootDir.absolutePath, "") .replace(file.name, "") - val longName = PgpActivity.getLongName( + val longName = BasePgpActivity.getLongName( fullPathToParent, rootDir.absolutePath, toString()) 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<ByteArray>, 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"> <androidx.appcompat.widget.AppCompatTextView - android:id="@+id/crypto_password_category_decrypt" + android:id="@+id/password_category" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" @@ -32,7 +32,7 @@ tools:text="CATEGORY HERE" /> <androidx.appcompat.widget.AppCompatTextView - android:id="@+id/crypto_password_file" + android:id="@+id/password_file" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/activity_horizontal_margin" @@ -41,11 +41,11 @@ android:textSize="24sp" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/crypto_password_category_decrypt" + app:layout_constraintTop_toBottomOf="@id/password_category" tools:text="PASSWORD FILE NAME HERE" /> <androidx.appcompat.widget.AppCompatTextView - android:id="@+id/crypto_password_last_changed" + android:id="@+id/password_last_changed" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" @@ -54,7 +54,7 @@ android:textIsSelectable="false" android:textSize="18sp" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/crypto_password_file" + app:layout_constraintTop_toBottomOf="@id/password_file" tools:text="LAST CHANGED HERE" /> @@ -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" /> <com.google.android.material.textfield.TextInputLayout diff --git a/app/src/main/res/layout/encrypt_layout.xml b/app/src/main/res/layout/password_creation_activity.xml index 02864135..13af597c 100644 --- a/app/src/main/res/layout/encrypt_layout.xml +++ b/app/src/main/res/layout/password_creation_activity.xml @@ -11,10 +11,10 @@ android:background="?android:attr/windowBackground" android:orientation="vertical" android:padding="@dimen/activity_horizontal_margin" - tools:context="com.zeapo.pwdstore.crypto.PgpActivity"> + tools:context="com.zeapo.pwdstore.crypto.PasswordCreationActivity"> <androidx.appcompat.widget.AppCompatEditText - android:id="@+id/crypto_password_category" + android:id="@+id/category" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/activity_horizontal_margin" @@ -33,10 +33,10 @@ android:layout_margin="8dp" android:hint="@string/crypto_name_hint" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/crypto_password_category"> + app:layout_constraintTop_toBottomOf="@id/category"> <com.google.android.material.textfield.TextInputEditText - android:id="@+id/crypto_password_file_edit" + android:id="@+id/filename" android:layout_width="match_parent" android:layout_height="wrap_content" /> </com.google.android.material.textfield.TextInputLayout> @@ -52,7 +52,7 @@ app:layout_constraintTop_toBottomOf="@id/name_input_layout"> <com.google.android.material.textfield.TextInputEditText - android:id="@+id/crypto_password_edit" + android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textVisiblePassword" /> @@ -77,14 +77,14 @@ app:layout_constraintTop_toBottomOf="@id/generate_password"> <com.google.android.material.textfield.TextInputEditText - android:id="@+id/crypto_extra_edit" + android:id="@+id/extra_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textMultiLine|textVisiblePassword" /> </com.google.android.material.textfield.TextInputLayout> - <Switch + <com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/encrypt_username" android:layout_width="match_parent" android:layout_height="wrap_content" 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 @@ -15,12 +15,6 @@ app:showAsAction="ifRoom" /> <item - android:id="@+id/menu_edit_password" - android:icon="@drawable/ic_edit_white_24dp" - android:title="@string/edit" - app:showAsAction="ifRoom" /> - - <item android:id="@+id/menu_delete_password" android:icon="@drawable/ic_delete_white_24dp" android:title="@string/delete" diff --git a/app/src/main/res/menu/pgp_handler_new_password.xml b/app/src/main/res/menu/pgp_handler_new_password.xml index c3e73199..78ea8ced 100644 --- a/app/src/main/res/menu/pgp_handler_new_password.xml +++ b/app/src/main/res/menu/pgp_handler_new_password.xml @@ -8,17 +8,17 @@ xmlns:tools="http://schemas.android.com/tools" tools:context="com.zeapo.pwdstore.crypto.PgpActivity"> <item - android:id="@+id/crypto_cancel_add" + android:id="@+id/cancel_password_add" android:icon="@drawable/ic_clear_white_24dp" android:title="@string/crypto_cancel" pwstore:showAsAction="ifRoom" /> <item - android:id="@+id/crypto_confirm_add" + android:id="@+id/save_password" android:icon="@drawable/ic_save_white_24dp" android:title="@string/crypto_save" pwstore:showAsAction="ifRoom" /> <item - android:id="@+id/crypto_confirm_add_and_copy" + android:id="@+id/save_and_copy_password" android:icon="@drawable/ic_save_copy_white_24dp" android:title="@string/crypto_save_and_copy" pwstore: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 @@ <string name="jgit_error_dialog_text">رسالة مِن jgit: \n</string> <!-- Git Handler --> - <string name="forget_username_dialog_text">هل نسيت إدخال إسم المستخدم ؟</string> <string name="ssh_preferences_dialog_title">خال من مفاتيح الـ SSH</string> <string name="ssh_preferences_dialog_import">إستيراد</string> <string name="ssh_preferences_dialog_generate">توليد</string> @@ -34,14 +33,9 @@ <string name="server_protocol">البروتوكول</string> <string name="server_url">عنوان الخادوم</string> <string name="server_port_hint">22</string> - <string name="default_ssh_port">22</string> - <string name="default_https_port">443</string> <string name="server_path">مسار المستودع</string> - <string name="server_path_hint">path/to/pass</string> <string name="server_user">إسم المستخدم</string> - <string name="server_user_hint">git_username</string> - <string name="server_resulting_url">عنوان الرابط الناتج</string> <string name="connection_mode">نوع المصادقة</string> <string name="git_user_name_hint">إسم المستخدم</string> @@ -87,7 +81,6 @@ <string name="pref_external_repository">مستودع تخزين خارجي</string> <string name="pref_external_repository_summary">إستخدم كلمة مرور المستودع الخارجي</string> <string name="pref_select_external_repository">إختيار مستودع التخزين الخارجي</string> - <string name="prefs_use_default_file_picker">إستخدم أداة إختيار الملفات الإفتراضي</string> <string name="prefs_export_passwords_title">تصدير كلمات السر</string> <string name="prefs_version">النسخة</string> @@ -105,7 +98,6 @@ <string name="ssh_keygen_comment">تعليق</string> <string name="ssh_keygen_generate">توليد</string> <string name="ssh_keygen_copy">نسخ</string> - <string name="ssh_keygen_show_passphrase">إظهار العبارة السرية</string> <!-- Misc --> <string name="dialog_ok">حسناً</string> @@ -119,7 +111,6 @@ <string name="refresh_list">تحديث القائمة</string> <string name="show_password">إظهار كلمة السر</string> <string name="show_extra">إظهار المزيد من المحتوى</string> - <string name="repository_uri">عنوان المستودع</string> <string name="app_icon_hint">أيقونة التطبيق</string> <string name="folder_icon_hint">أيقونة المجلد</string> 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 @@ <string name="password_exists_message">Toto přepíše%1$s %2$s .</string> <!-- git commits --> - <string name="git_commit_add_text">Přídat generované heslo pro %1$s s použítím android password store.</string> - <string name="git_commit_edit_text">Upravit heslo %1$s s použítím android password store.</string> + <string name="git_commit_add_text">Přídat generované heslo pro %1$s s použítím Android Password Store.</string> + <string name="git_commit_edit_text">Upravit heslo %1$s s použítím Android Password Store.</string> <string name="git_commit_remove_text">Odstranit %1$s ze store. </string> <!-- PGPHandler --> <string name="provider_toast_text">Nebyl vybrán poskytovatel OpenPGP!</string> @@ -43,7 +43,6 @@ <string name="jgit_error_dialog_text">Zpráva od jgit: \n</string> <!-- Git Handler --> - <string name="forget_username_dialog_text">Zapomněli jste uvést přihlašovací jméno?</string> <string name="ssh_preferences_dialog_text">Importujte nebo si prosím vygenerujte svůj SSH klíč v nastavení aplikace</string> <string name="ssh_preferences_dialog_title">Žádný SSH klíč</string> <string name="ssh_preferences_dialog_import">Import</string> @@ -64,21 +63,13 @@ <string name="server_protocol">Protokol</string> <string name="server_url">URL serveru</string> <string name="server_port_hint">22</string> - <string name="default_ssh_port">22</string> - <string name="default_https_port">443</string> <string name="server_path">Cesta k repozitáři</string> - <string name="server_path_hint">cesta/k/heslům</string> <string name="server_user">Jméno</string> - <string name="server_user_hint">git_username</string> - <string name="server_resulting_url">Výsledná URL</string> <string name="connection_mode">Mód ověření</string> - <string name="warn_malformed_url_port">Při použití vlastního portu, zadejte absolutní cestu (začíná "/")</string> - <string name="git_user_name_hint">Jméno</string> <string name="git_user_email">Email</string> - <string name="git_user_email_hint">email</string> <string name="invalid_email_dialog_text">Zadejte prosím platnou emailovou adresu</string> <string name="clone_button">Klonovat!</string> @@ -154,13 +145,11 @@ <string name="ssh_keygen_generate">Generovat</string> <string name="ssh_keygen_copy">Kopírovat</string> <string name="ssh_keygen_tip">Přidat tento veřejný klíč na Git server.</string> - <string name="ssh_keygen_show_passphrase">Zobrazit bezpečnostní frázi</string> <!-- Misc --> <string name="dialog_ok">OK</string> <string name="dialog_yes">Ano</string> <string name="dialog_no">Ne</string> - <string name="dialog_oops">Ajaj…</string> <string name="dialog_cancel">Zrušit</string> <string name="git_sync">Synchronizovat repozitář</string> <string name="git_pull">Stáhnout ze serveru</string> 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 @@ <string name="jgit_error_dialog_text">Message from jgit: \n</string> <!-- Git Handler --> - <string name="forget_username_dialog_text">Hast du vergessen einen Nutzernamen zu vergeben?</string> <string name="ssh_preferences_dialog_text">Please import or generate your SSH key file in the preferences</string> <string name="ssh_preferences_dialog_title">Kein SSH-Key angegeben</string> <string name="ssh_preferences_dialog_import">Import</string> @@ -45,18 +44,11 @@ <string name="server_protocol">Protokoll</string> <string name="server_url">Server URL</string> <string name="server_port_hint">22</string> - <string name="default_ssh_port">22</string> - <string name="default_https_port">443</string> <string name="server_path">Repo-Pfad</string> - <string name="server_path_hint">path/to/pass</string> <string name="server_user">Nutzername</string> - <string name="server_user_hint">Git-Nutzername</string> - <string name="server_resulting_url">Erzeugte URL</string> <string name="connection_mode">Authentifizierungsmethode</string> - <string name="warn_malformed_url_port">Wenn du einen anderen Port nutzt, setze den absoluten Pfad (startet mit "/")</string> - <string name="git_user_name_hint">Nutzername</string> <string name="invalid_email_dialog_text">Bitte valide Email eingeben</string> <string name="clone_button">Klone!</string> @@ -112,7 +104,6 @@ <string name="pref_external_repository">Externes Repository</string> <string name="pref_external_repository_summary">Nutze ein externes Repository</string> <string name="pref_select_external_repository">Wähle ein externes Repository</string> - <string name="prefs_use_default_file_picker">Benutze Standardauswahl für Dateien</string> <string name="prefs_export_passwords_title">Passwörter exportieren</string> <string name="prefs_export_passwords_summary">Exportiert die verschlüsselten Passwörter in ein externes Verzeichnis</string> <string name="prefs_version">Version</string> @@ -132,13 +123,11 @@ <string name="ssh_keygen_generate">Generieren</string> <string name="ssh_keygen_copy">Kopieren</string> <string name="ssh_keygen_tip">Füge den Public-Key zu deinem Git-Server hinzu.</string> - <string name="ssh_keygen_show_passphrase">Zeige Passwort</string> <!-- Misc --> <string name="dialog_ok">OK</string> <string name="dialog_yes">Ja</string> <string name="dialog_no">Nein</string> - <string name="dialog_oops">Oops…</string> <string name="dialog_cancel">Abbruch</string> <string name="git_sync">Synchronisiere Repository</string> <string name="git_pull">Git Pull</string> @@ -153,7 +142,6 @@ <string name="send_plaintext_password_to">Passwort senden als Nur-Text mit behilfe von…</string> <string name="show_password">Password wiedergeben</string> <string name="show_extra">Zeige weiteren Inhalt</string> - <string name="repository_uri">Repository URI</string> <string name="app_icon_hint">App Icon</string> <string name="folder_icon_hint">Verzeichnis Icon</string> 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 @@ <string name="jgit_error_dialog_text">Mensaje de jgit: \n</string> <!-- Git Handler --> - <string name="forget_username_dialog_text">Olvidaste especificar un nombre de usuario?</string> <string name="ssh_preferences_dialog_text">Por favor importa o genera tu llave SSH en los ajustes</string> <string name="ssh_preferences_dialog_title">No hay llave SSH</string> <string name="ssh_preferences_dialog_import">Importar</string> @@ -61,18 +60,11 @@ <string name="server_protocol">Protocolo</string> <string name="server_url">URL de servidor</string> <string name="server_port_hint">22</string> - <string name="default_ssh_port">22</string> - <string name="default_https_port">443</string> <string name="server_path">Ruta del repositorio</string> - <string name="server_path_hint">ruta/a/claves</string> <string name="server_user">Nombre de usuario</string> - <string name="server_user_hint">nombre_usuario</string> - <string name="server_resulting_url">URL resultante</string> <string name="connection_mode">Modo de autenticación</string> - <string name="warn_malformed_url_port">Al usar puertos personalizados, ingresa una ruta absoluta (empieza con "/")</string> - <string name="git_user_name_hint">Nombre de usuario</string> <string name="invalid_email_dialog_text">Por favor ingresa una dirección de correo</string> <string name="clone_button">¡Clonar!</string> @@ -134,7 +126,6 @@ <string name="pref_external_repository">Repositorio externo</string> <string name="pref_external_repository_summary">Usar un repositorio externo para contraseñas</string> <string name="pref_select_external_repository">Seleccionar repositorio externo</string> - <string name="prefs_use_default_file_picker">Usar seleccionador de archivos por defecto</string> <string name="prefs_export_passwords_title">Exportar contraseñas</string> <string name="prefs_export_passwords_summary">Exporta las contraseñas cifradas a un directorio externo.</string> <string name="prefs_version">Versión</string> @@ -160,13 +151,11 @@ <string name="ssh_keygen_generate">Generar</string> <string name="ssh_keygen_copy">Copiar</string> <string name="ssh_keygen_tip">Registra esta llave pública en tu servidor Git.</string> - <string name="ssh_keygen_show_passphrase">Mostrar contraseña</string> <!-- Misc --> <string name="dialog_ok">OK</string> <string name="dialog_yes">Sí</string> <string name="dialog_no">No</string> - <string name="dialog_oops">Ups…</string> <string name="dialog_cancel">Cancelar</string> <string name="git_sync">Sincronizar con servidor</string> <string name="git_pull">Descargar del servidor</string> @@ -181,7 +170,6 @@ <string name="send_plaintext_password_to">Enviar contraseña en texto plano usando…</string> <string name="show_password">Mostrar contraseña</string> <string name="show_extra">Mostrar contenido extra</string> - <string name="repository_uri">URI del repositorio</string> <string name="app_icon_hint">Ícono de app</string> <string name="folder_icon_hint">Ícono de directorio</string> @@ -212,6 +200,5 @@ <string name="hackish_tools">Hackish tools</string> <string name="abort_rebase">Abortar rebase</string> <string name="commit_hash">Hash del commit</string> - <string name="crypto_extra_edit_hint">Username: Nombre de usuario\n… o algún contenido extra</string> <string name="get_last_changed_failed">Error al obtener la fecha de último cambio</string> </resources> 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 @@ <string name="jgit_error_dialog_text">Message de jgit: \n</string> <!-- Git Handler --> - <string name="forget_username_dialog_text">Avez-vous oublié de renseigner votre nom d\'utilisateur ?</string> <string name="ssh_preferences_dialog_text">Vous devez importer ou générer votre fichier de clef SSH dans les préférences</string> <string name="ssh_preferences_dialog_title">Absence de clef SSH</string> <string name="ssh_preferences_dialog_import">Importer</string> @@ -67,21 +66,13 @@ <string name="server_protocol">Protocole</string> <string name="server_url">URL du serveur</string> <string name="server_port_hint">22</string> - <string name="default_ssh_port">22</string> - <string name="default_https_port">443</string> <string name="server_path">Chemin du dépôt</string> - <string name="server_path_hint">path/to/pass</string> <string name="server_user">Nom d\'utilisateur</string> - <string name="server_user_hint">git_username</string> - <string name="server_resulting_url">URL finale</string> <string name="connection_mode">Méthode d\'authentification</string> - <string name="warn_malformed_url_port">Lors de l\'utilisation d\'un numéro de port personnalisé, fournissez un chemin absolu (commençant par "/")</string> - <string name="git_user_name_hint">Nom d\'utilisateur</string> <string name="git_user_email">Email</string> - <string name="git_user_email_hint">email</string> <string name="invalid_email_dialog_text">Merci de saisir une adresse mail valide</string> <string name="clone_button">Cloner !</string> @@ -139,7 +130,6 @@ <string name="pref_external_repository">Dépôt externe</string> <string name="pref_external_repository_summary">Utilise un dépôt externe pour les mots de passe</string> <string name="pref_select_external_repository">Choisissez un dépôt externe</string> - <string name="prefs_use_default_file_picker">Utiliser le selecteur de fichier par défaut</string> <string name="prefs_export_passwords_title">Exporter les mots de passe</string> <string name="prefs_export_passwords_summary">Exporter les mots de passe (chiffrés) vers un répertoire externe</string> <string name="prefs_version">Version</string> @@ -161,13 +151,11 @@ <string name="ssh_keygen_generate">Générer</string> <string name="ssh_keygen_copy">Copier</string> <string name="ssh_keygen_tip">Enregistrez cette clef publique sur votre serveur Git.</string> - <string name="ssh_keygen_show_passphrase">Afficher le mot de passe</string> <!-- Misc --> <string name="dialog_ok">OK</string> <string name="dialog_yes">Oui</string> <string name="dialog_no">Non</string> - <string name="dialog_oops">Oups…</string> <string name="dialog_cancel">Annuler</string> <string name="git_sync">Synchronisation du dépôt</string> <string name="git_pull">Importer du serveur</string> @@ -182,7 +170,6 @@ <string name="send_plaintext_password_to">Envoyer le mot de passe en clair via…</string> <string name="show_password">Montrer le mot de passe</string> <string name="show_extra">Afficher le contenu supplémentaire</string> - <string name="repository_uri">Adresse du dépot</string> <string name="app_icon_hint">Icône de l\'application</string> <string name="folder_icon_hint">Icône du dossier</string> @@ -212,6 +199,5 @@ <string name="git_operation_remember_passphrase">Se rappeler de la phrase secrète dans la configuration de l\'application (peu sûr)</string> <string name="hackish_tools">Outils de hack</string> <string name="commit_hash">Commettre la clé</string> - <string name="crypto_extra_edit_hint">nom d\'utilisateur: quelque chose d\'autre contenu supplémentaire</string> <string name="get_last_changed_failed">Failed to get last changed date</string> </resources> 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 @@ <string name="jgit_error_dialog_text">jgit からのメッセージ: \n</string> <!-- Git Handler --> - <string name="forget_username_dialog_text">ユーザー名の指定を忘れましたか?</string> <string name="ssh_preferences_dialog_text">プリファレンスで SSH 鍵ファイルをインポートまたは生成してください</string> <string name="ssh_preferences_dialog_title">SSH 鍵がありませんkey</string> <string name="ssh_preferences_dialog_import">インポート</string> @@ -46,18 +45,11 @@ <string name="server_protocol">プロトコル</string> <string name="server_url">サーバー URL</string> <string name="server_port_hint">22</string> - <string name="default_ssh_port">22</string> - <string name="default_https_port">443</string> <string name="server_path">リポジトリのパス</string> - <string name="server_path_hint">path/to/pass</string> <string name="server_user">ユーザー名</string> - <string name="server_user_hint">git_username</string> - <string name="server_resulting_url">結果 URL</string> <string name="connection_mode">認証モード</string> - <string name="warn_malformed_url_port">カスタムポートを使用する場合は、絶対パスを入力 ("/" で始まる)</string> - <string name="git_user_name_hint">ユーザー名</string> <!-- PGP Handler --> <string name="crypto_name_hint">名前</string> @@ -114,13 +106,11 @@ <string name="ssh_keygen_generate">生成</string> <string name="ssh_keygen_copy">コピー</string> <string name="ssh_keygen_tip">この公開鍵を Git サーバーに提供してください。</string> - <string name="ssh_keygen_show_passphrase">パスフレーズを表示</string> <!-- Misc --> <string name="dialog_ok">OK</string> <string name="dialog_yes">はい</string> <string name="dialog_no">いいえ</string> - <string name="dialog_oops">おっと…</string> <string name="dialog_cancel">キャンセル</string> <string name="git_sync">リポジトリを同期</string> <string name="git_pull">リモートからプル</string> 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 @@ <color name="primary_light_color">#FF373737</color> <color name="primary_dark_color">#FF000000</color> <color name="secondary_color">#FFFF7539</color> - <color name="secondary_light_color">#FFFFa667</color> - <color name="secondary_dark_color">#FFC54506</color> <color name="primary_text_color">#FFFFFFFF</color> - <color name="secondary_text_color">#FFFFFFFF</color> <!-- Theme variables --> <color name="window_background">@color/primary_color</color> - <color name="color_surface">#FF111111</color> <color name="navigation_bar_color">@color/primary_color</color> <color name="list_multiselect_background">#66EEEEEE</color> <color name="status_bar_color">@color/window_background</color> 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 @@ <string name="jgit_error_dialog_text">Сообщение от jgit: \n</string> <!-- Git Handler --> - <string name="forget_username_dialog_text">Вы забыли указать имя пользователя?</string> <string name="ssh_preferences_dialog_text">Пожалуйста, импортируйте или сгенерируйте новый SSH ключ в настройках</string> <string name="ssh_preferences_dialog_title">Нет SSH ключа</string> <string name="ssh_preferences_dialog_import">Импортировать</string> @@ -69,21 +68,13 @@ <string name="server_protocol">Протокол</string> <string name="server_url">URL сервера</string> <string name="server_port_hint">22</string> - <string name="default_ssh_port">22</string> - <string name="default_https_port">443</string> <string name="server_path">Путь к репозиторию</string> - <string name="server_path_hint">путь/до/пароля</string> <string name="server_user">Имя пользователя</string> - <string name="server_user_hint">git_username</string> - <string name="server_resulting_url">Получившийся URL</string> <string name="connection_mode">Тип авторизации</string> - <string name="warn_malformed_url_port">При использовании нестандартных портов, укажите полный путь (начинается с "/")</string> - <string name="git_user_name_hint">Имя пользователя</string> <string name="git_user_email">Электронная почта</string> - <string name="git_user_email_hint">электронная почта</string> <string name="invalid_email_dialog_text">Введите корректный email</string> <string name="clone_button">Клонировать</string> @@ -149,7 +140,6 @@ <string name="pref_external_repository">Внешний репозиторий</string> <string name="pref_external_repository_summary">Использовать внешний репозиторий</string> <string name="pref_select_external_repository">Выбрать внешний репозиторий</string> - <string name="prefs_use_default_file_picker">Использовать стандартное окно выбора файлов</string> <string name="prefs_export_passwords_title">Экспортировать пароли</string> <string name="prefs_export_passwords_summary">Экспортировать пароли в открытом виде во внешнее хранилище</string> <string name="prefs_version">Версия</string> @@ -193,13 +183,11 @@ <string name="ssh_keygen_generate">Сгенерировать</string> <string name="ssh_keygen_copy">Скоприровать</string> <string name="ssh_keygen_tip">Поместите публичный ключ на сервер Git</string> - <string name="ssh_keygen_show_passphrase">Показать пароль</string> <!-- Misc --> <string name="dialog_ok">OK</string> <string name="dialog_yes">Да</string> <string name="dialog_no">Нет</string> - <string name="dialog_oops">Упс…</string> <string name="dialog_cancel">Отмена</string> <string name="git_sync">Синхронизировать репозиторий</string> <string name="git_pull">Пулл с удаленного сервера</string> @@ -215,7 +203,6 @@ <string name="show_password">Показать пароль</string> <string name="show_extra">Показать дополнительную информацию</string> <string name="hide_extra">Скрыть расширенный контекст</string> - <string name="repository_uri">URI репозитория</string> <string name="app_icon_hint">Иконка приложения</string> <string name="folder_icon_hint">Иконка папки</string> @@ -226,9 +213,7 @@ <string name="oreo_autofill_save_internal_error">Сохранение не удалось из-за внутренней ошибки</string> <string name="oreo_autofill_save_app_not_supported">Это приложение в настоящее время не поддерживается</string> <string name="oreo_autofill_save_passwords_dont_match">Пароли не совпадают</string> - <string name="oreo_autofill_save_invalid_password">Невозможно извлечь пароль, пожалуйста, используйте другой браузер</string> <string name="oreo_autofill_generate_password">Сгенерировать пароль...</string> - <string name="oreo_autofill_publisher_changed">Издатель приложения изменился; это может быть попытка фишинга.</string> <string name="oreo_autofill_max_matches_reached">Достигнуто максимальное количество совпадений (%1$d); очистите совпадения перед тем как добавите новые.</string> <string name="oreo_autofill_warning_publisher_header">Издатель приложения изменился с тех пор как вы первый раз связали с ним запись хранилища паролей:</string> <string name="oreo_autofill_warning_publisher_footer"><b>Установленное приложение может попытаться украсть ваши учетные данные, выдавая себя за доверенное приложение</b>\n\nПопробуйте удалить или переустановить приложение из доверенного источника, такого как Play Store, Amazon Appstore, F-Droid или магазин приложений производителя вашего смартфона.</string> @@ -275,7 +260,6 @@ <string name="abort_rebase">Прервать перебазирование и записать изменения в новую ветку</string> <string name="reset_to_remote">Полный сброс до состояния удаленной ветки</string> <string name="commit_hash">Хэш-сумма изменений</string> - <string name="crypto_extra_edit_hint">имя пользователя: какой-то другой дополнительный контент</string> <string name="get_last_changed_failed">Failed to get last changed date</string> <string name="openkeychain_ssh_api_connect_fail">Ошибка при подключении к сервису OpenKeychain SSH API</string> <string name="no_ssh_api_provider">Не найдено SSH API провайдеров. OpenKeychain установлен?</string> 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 @@ <string name="jgit_error_dialog_text">Message from jgit:</string> <!-- Git Handler --> - <string name="forget_username_dialog_text">你忘了提供用户名了吗?</string> <string name="ssh_preferences_dialog_text">请在设置中导入或生成你的SSH密钥文件</string> <string name="ssh_preferences_dialog_title">无SSH密钥</string> <string name="ssh_preferences_dialog_import">导入</string> @@ -46,18 +45,11 @@ <string name="server_protocol">接口</string> <string name="server_url">服务器 URL</string> <string name="server_port_hint">22</string> - <string name="default_ssh_port">22</string> - <string name="default_https_port">443</string> <string name="server_path">Repo 路径</string> - <string name="server_path_hint">path/to/pass</string> <string name="server_user">用户名</string> - <string name="server_user_hint">git_username</string> - <string name="server_resulting_url">生成的 URL</string> <string name="connection_mode">认证模式</string> - <string name="warn_malformed_url_port">如果使用自定义端口, 请提供绝对路径 (从根目录开始)</string> - <string name="git_user_name_hint">用户名</string> <!-- PGP Handler --> <string name="crypto_name_hint">名称</string> @@ -111,13 +103,11 @@ <string name="ssh_keygen_generate">生成</string> <string name="ssh_keygen_copy">复制</string> <string name="ssh_keygen_tip">在你的Git服务器上提供此公钥</string> - <string name="ssh_keygen_show_passphrase">显示口令</string> <!-- Misc --> <string name="dialog_ok">确定</string> <string name="dialog_yes">确定</string> <string name="dialog_no">否</string> - <string name="dialog_oops">糟糕…</string> <string name="dialog_cancel">取消</string> <string name="git_sync">同步 Repo</string> <string name="git_pull">Git Pull</string> 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 @@ <string name="jgit_error_dialog_text">Message from jgit:</string> <!-- Git Handler --> - <string name="forget_username_dialog_text">你忘記輸入使用者名稱了嗎?</string> <string name="ssh_preferences_dialog_text">請在設定中匯入或產生你的 SSH 金鑰</string> <string name="ssh_preferences_dialog_title">無 SSH 金鑰</string> <string name="ssh_preferences_dialog_import">匯入</string> @@ -43,18 +42,11 @@ <string name="server_protocol">port</string> <string name="server_url">伺服器 URL</string> <string name="server_port_hint">22</string> - <string name="default_ssh_port">22</string> - <string name="default_https_port">443</string> <string name="server_path">Repo 路徑</string> - <string name="server_path_hint">path/to/pass</string> <string name="server_user">使用者名稱</string> - <string name="server_user_hint">git_username</string> - <string name="server_resulting_url">生成的 URL</string> <string name="connection_mode">認證模式</string> - <string name="warn_malformed_url_port">如果使用自定 port, 請使用绝對路徑 (從根目錄開始)</string> - <string name="git_user_name_hint">使用者名稱</string> <!-- PGP Handler --> <string name="crypto_name_hint">名稱</string> @@ -108,13 +100,11 @@ <string name="ssh_keygen_generate">產生</string> <string name="ssh_keygen_copy">複製</string> <string name="ssh_keygen_tip">在你的 Git 伺服器上提供此公鑰</string> - <string name="ssh_keygen_show_passphrase">顯示密碼</string> <!-- Misc --> <string name="dialog_ok">確定</string> <string name="dialog_yes">確定</string> <string name="dialog_no">否</string> - <string name="dialog_oops">糟糕…</string> <string name="dialog_cancel">取消</string> <string name="git_sync">同步 Repo</string> <string name="git_pull">Git Pull</string> 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 @@ --> <resources> - <string-array name="connection_modes" translatable="false"> - <item>ssh-key</item> - <item>username/password</item> - <item>OpenKeychain</item> - </string-array> - <string-array name="clone_protocols" translatable="false"> - <item>ssh://</item> - <item>https://</item> - </string-array> <string-array name="sort_order_entries"> <item>@string/pref_folder_first_sort_order</item> <item>@string/pref_file_first_sort_order</item> @@ -23,12 +14,6 @@ <item>FILE_FIRST</item> <item>INDEPENDENT</item> </string-array> - <string-array name="capitalization_type_entries"> - <item>0</item> - <item>1</item> - <item>2</item> - <item>3</item> - </string-array> <string-array name="capitalization_type_values"> <item>lowercase</item> <item>UPPERCASE</item> 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 @@ <color name="primary_light_color">#8eacbb</color> <color name="primary_dark_color">#34515e</color> <color name="secondary_color">#ff7043</color> - <color name="secondary_light_color">#ffa270</color> - <color name="secondary_dark_color">#c63f17</color> <color name="primary_text_color">#212121</color> - <color name="secondary_text_color">#ffffff</color> <color name="white">#ffffffff</color> <!-- Theme variables --> <color name="window_background">#eceff1</color> <color name="ic_launcher_background">#D4F1EA</color> <color name="color_control_normal">@color/primary_text_color</color> - <color name="color_surface">#FFFFFF</color> <color name="list_multiselect_background">#668eacbb</color> <color name="navigation_bar_color">#000000</color> <color name="status_bar_color">@color/primary_dark_color</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 @@ <dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen> <dimen name="fab_compat_margin">16dp</dimen> - <dimen name="fab_margin">8dp</dimen> <dimen name="normal_margin">8dp</dimen> <dimen name="bottom_sheet_item_height">56dp</dimen> </resources> 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 @@ <string name="password_exists_message">This will overwrite %1$s with %2$s.</string> <!-- git commits --> - <string name="git_commit_add_text">Add generated password for %1$s using android password store.</string> - <string name="git_commit_edit_text">Edit password for %1$s using android password store.</string> + <string name="git_commit_add_text">Add generated password for %1$s using Android Password Store.</string> + <string name="git_commit_edit_text">Edit password for %1$s using Android Password Store.</string> <string name="git_commit_remove_text">Remove %1$s from store.</string> <string name="git_commit_move_text">Rename %1$s to %2$s.</string> @@ -58,7 +58,6 @@ <string name="jgit_error_dialog_text">Message from jgit: \n</string> <!-- Git Handler --> - <string name="forget_username_dialog_text">Did you forget to specify a username?</string> <string name="set_information_dialog_text">Please fix the remote server configuration in settings before proceeding</string> <string name="ssh_preferences_dialog_text">Please import or generate your SSH key file in the preferences</string> <string name="ssh_preferences_dialog_title">No SSH key</string> @@ -81,26 +80,15 @@ <string name="server_name">Server</string> <string name="server_protocol">Protocol</string> <string name="server_url">Server URL</string> - <string name="server_url_hint" translatable="false">server.com</string> <string name="server_port_hint">Port</string> - <string name="default_ssh_port">22</string> - <string name="default_https_port">443</string> <string name="server_path">Repo path</string> - <string name="server_path_hint">path/to/pass</string> <string name="server_user">Username</string> - <string name="server_user_hint">git_username</string> - <string name="server_resulting_url">Resulting URL</string> <string name="connection_mode">Authentication Mode</string> - <string name="warn_malformed_url_port">When using custom ports, provide an absolute path (starts with "/")</string> - <!-- Git Config fragment --> - <string name="git_config" translatable="false">Git config</string> - <string name="git_user_name" translatable="false">Username</string> <string name="git_user_name_hint">Username</string> <string name="git_user_email">Email</string> - <string name="git_user_email_hint">email</string> <string name="invalid_email_dialog_text">Please enter a valid email address</string> <string name="clone_button">Clone</string> @@ -172,7 +160,6 @@ <string name="pref_external_repository">External repository</string> <string name="pref_external_repository_summary">Use an external password repository</string> <string name="pref_select_external_repository">Select external repository</string> - <string name="prefs_use_default_file_picker">Use default file picker</string> <string name="prefs_export_passwords_title">Export passwords</string> <string name="prefs_export_passwords_summary">Exports the encrypted passwords to an external directory</string> <string name="prefs_version">Version</string> @@ -216,7 +203,6 @@ <string name="ssh_keygen_generate">Generate</string> <string name="ssh_keygen_copy">Copy</string> <string name="ssh_keygen_tip">Provide this public key to your Git server.</string> - <string name="ssh_keygen_show_passphrase">Show passphrase</string> <string name="ssh_key_gen_generating_progress">Generating keys…</string> <string name="ssh_keygen_generating_done">Done!</string> <string name="key_length_2048" translatable="false">2048</string> @@ -228,7 +214,6 @@ <string name="dialog_no">No</string> <string name="dialog_positive">Go to Settings</string> <string name="dialog_negative">Go back</string> - <string name="dialog_oops">Oops…</string> <string name="dialog_cancel">Cancel</string> <string name="git_sync">Synchronize repository</string> <string name="git_pull">Pull from remote</string> @@ -244,7 +229,6 @@ <string name="show_password">Show password</string> <string name="show_extra">Show extra content</string> <string name="hide_extra">Hide extra content</string> - <string name="repository_uri">Repository URI</string> <string name="app_icon_hint">App icon</string> <string name="folder_icon_hint">Folder icon</string> @@ -257,9 +241,7 @@ <string name="oreo_autofill_save_internal_error">Save failed due to an internal error</string> <string name="oreo_autofill_save_app_not_supported">This app is currently not supported</string> <string name="oreo_autofill_save_passwords_dont_match">Passwords don\'t match</string> - <string name="oreo_autofill_save_invalid_password">Couldn\'t extract password, please use a different browser for now</string> <string name="oreo_autofill_generate_password">Generate password…</string> - <string name="oreo_autofill_publisher_changed">The app\'s publisher has changed; this may be a phishing attempt.</string> <string name="oreo_autofill_max_matches_reached">Maximum number of matches (%1$d) reached; clear matches before adding new ones.</string> <string name="oreo_autofill_warning_publisher_header">This app\'s publisher has changed since you first associated a Password Store entry with it:</string> <string name="oreo_autofill_warning_publisher_footer"><b>The currently installed app may be trying to steal your credentials by pretending to be a trusted app.</b>\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.</string> @@ -313,7 +295,6 @@ <string name="abort_rebase">Abort rebase and push new branch</string> <string name="reset_to_remote">Hard reset to remote branch</string> <string name="commit_hash">Commit hash</string> - <string name="crypto_extra_edit_hint">username: something other extra content</string> <string name="get_last_changed_failed">Failed to get last changed date</string> <string name="openkeychain_ssh_api_connect_fail">Failed to connect to OpenKeychain SSH API service.</string> <string name="no_ssh_api_provider">No SSH API provider found. Is OpenKeychain installed?</string> @@ -376,4 +357,13 @@ <string name="preference_custom_public_suffixes_title">Custom domains</string> <string name="preference_custom_public_suffixes_summary">Autofill will distinguish subdomains of these domains</string> <string name="preference_custom_public_suffixes_hint">company.com\npersonal.com</string> + + <!-- OpenKeychain errors --> + <string name="openpgp_error_wrong_passphrase">Incorrect passphrase</string> + <string name="openpgp_error_no_user_ids">No matching PGP keys found</string> + <string name="openpgp_error_unknown">Error from OpenKeyChain : %s</string> + + <!-- Password creation failure --> + <string name="password_creation_file_write_fail_title">Error</string> + <string name="password_creation_file_write_fail_message">Failed to write password file to the store, please try again.</string> </resources> |