Break down PGP Activity into focused sections (#776)

This commit is contained in:
Harsh Shandilya 2020-06-12 14:58:15 +00:00 committed by GitHub
parent bf33fb2c88
commit d8231e112a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1067 additions and 1163 deletions

View file

@ -10,12 +10,14 @@ All notable changes to this project will be documented in this file.
- Completely revamped decypted password view
- Add support for better, more secure Keyex's and MACs with a brand new SSH backend
- Allow manually marking domains for subdomain-level association. This will allow you to keep separate passwords for `site1.example.com` and `site2.example.com` and have them show as such in Autofill.
- Provide better messages for OpenKeychain errors
### Changed
- **BREAKING**: Remove support for HOTP/TOTP secrets - Please use FIDO keys or a dedicated app like [Aegis](https://github.com/beemdevelopment/Aegis) or [andOTP](https://github.com/andOTP/andOTP)
- Reduce Autofill false positives on username fields by removing "name" from list of heuristic terms
- Reduced app size
- Improve IME experience with server config screen
- Removed edit password option from long-press menu.
## [1.8.1] - 2020-05-24

View file

@ -4,7 +4,6 @@
*/
plugins {
id 'kotlin-android'
id 'kotlin-android-extensions'
}
final def keystorePropertiesFile = rootProject.file 'keystore.properties'
@ -83,7 +82,6 @@ dependencies {
implementation deps.androidx.lifecycle_common
implementation deps.androidx.lifecycle_livedata_ktx
implementation deps.androidx.lifecycle_viewmodel_ktx
implementation deps.androidx.local_broadcast_manager
implementation deps.androidx.material
implementation deps.androidx.preference
implementation deps.androidx.recycler_view

View file

@ -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"

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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

View file

@ -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()
}
}
}

View file

@ -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()

View file

@ -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"
/**

View file

@ -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")!!))
}
}
}
}

View file

@ -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
)

View file

@ -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
}
}
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -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"
}
}

View file

@ -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
}
}
}
}

View file

@ -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)) {

View file

@ -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

View file

@ -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)
}

View file

@ -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)) { _, _ -> }

View file

@ -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)
}

View file

@ -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))
}
}
}
}
}

View file

@ -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

View file

@ -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())

View file

@ -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? {

View file

@ -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

View file

@ -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"

View file

@ -14,12 +14,6 @@
android:title="@string/move"
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"

View file

@ -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" />

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"></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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -35,7 +35,6 @@ ext.deps = [
lifecycle_common: 'androidx.lifecycle:lifecycle-common-java8:2.3.0-alpha04',
lifecycle_livedata_ktx: 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha04',
lifecycle_viewmodel_ktx: 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha04',
local_broadcast_manager: 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0-alpha01',
material: 'com.google.android.material:material:1.3.0-alpha01',
preference: 'androidx.preference:preference:1.1.1',
recycler_view: 'androidx.recyclerview:recyclerview:1.2.0-alpha03',