Break down PGP Activity into focused sections (#776)
This commit is contained in:
parent
bf33fb2c88
commit
d8231e112a
44 changed files with 1067 additions and 1163 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
||||
/**
|
||||
|
|
|
@ -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")!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
270
app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
Normal file
270
app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
202
app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt
Normal file
202
app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)) { _, _ -> }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
<string name="jgit_error_dialog_text">Mensaje de jgit: \n</string>
|
||||
|
||||
<!-- Git Handler -->
|
||||
<string name="forget_username_dialog_text">Olvidaste especificar un nombre de usuario?</string>
|
||||
<string name="ssh_preferences_dialog_text">Por favor importa o genera tu llave SSH en los ajustes</string>
|
||||
<string name="ssh_preferences_dialog_title">No hay llave SSH</string>
|
||||
<string name="ssh_preferences_dialog_import">Importar</string>
|
||||
|
@ -61,18 +60,11 @@
|
|||
<string name="server_protocol">Protocolo</string>
|
||||
<string name="server_url">URL de servidor</string>
|
||||
<string name="server_port_hint">22</string>
|
||||
<string name="default_ssh_port">22</string>
|
||||
<string name="default_https_port">443</string>
|
||||
<string name="server_path">Ruta del repositorio</string>
|
||||
<string name="server_path_hint">ruta/a/claves</string>
|
||||
<string name="server_user">Nombre de usuario</string>
|
||||
<string name="server_user_hint">nombre_usuario</string>
|
||||
|
||||
<string name="server_resulting_url">URL resultante</string>
|
||||
<string name="connection_mode">Modo de autenticación</string>
|
||||
|
||||
<string name="warn_malformed_url_port">Al usar puertos personalizados, ingresa una ruta absoluta (empieza con "/")</string>
|
||||
|
||||
<string name="git_user_name_hint">Nombre de usuario</string>
|
||||
<string name="invalid_email_dialog_text">Por favor ingresa una dirección de correo</string>
|
||||
<string name="clone_button">¡Clonar!</string>
|
||||
|
@ -134,7 +126,6 @@
|
|||
<string name="pref_external_repository">Repositorio externo</string>
|
||||
<string name="pref_external_repository_summary">Usar un repositorio externo para contraseñas</string>
|
||||
<string name="pref_select_external_repository">Seleccionar repositorio externo</string>
|
||||
<string name="prefs_use_default_file_picker">Usar seleccionador de archivos por defecto</string>
|
||||
<string name="prefs_export_passwords_title">Exportar contraseñas</string>
|
||||
<string name="prefs_export_passwords_summary">Exporta las contraseñas cifradas a un directorio externo.</string>
|
||||
<string name="prefs_version">Versión</string>
|
||||
|
@ -160,13 +151,11 @@
|
|||
<string name="ssh_keygen_generate">Generar</string>
|
||||
<string name="ssh_keygen_copy">Copiar</string>
|
||||
<string name="ssh_keygen_tip">Registra esta llave pública en tu servidor Git.</string>
|
||||
<string name="ssh_keygen_show_passphrase">Mostrar contraseña</string>
|
||||
|
||||
<!-- Misc -->
|
||||
<string name="dialog_ok">OK</string>
|
||||
<string name="dialog_yes">Sí</string>
|
||||
<string name="dialog_no">No</string>
|
||||
<string name="dialog_oops">Ups…</string>
|
||||
<string name="dialog_cancel">Cancelar</string>
|
||||
<string name="git_sync">Sincronizar con servidor</string>
|
||||
<string name="git_pull">Descargar del servidor</string>
|
||||
|
@ -181,7 +170,6 @@
|
|||
<string name="send_plaintext_password_to">Enviar contraseña en texto plano usando…</string>
|
||||
<string name="show_password">Mostrar contraseña</string>
|
||||
<string name="show_extra">Mostrar contenido extra</string>
|
||||
<string name="repository_uri">URI del repositorio</string>
|
||||
<string name="app_icon_hint">Ícono de app</string>
|
||||
<string name="folder_icon_hint">Ícono de directorio</string>
|
||||
|
||||
|
@ -212,6 +200,5 @@
|
|||
<string name="hackish_tools">Hackish tools</string>
|
||||
<string name="abort_rebase">Abortar rebase</string>
|
||||
<string name="commit_hash">Hash del commit</string>
|
||||
<string name="crypto_extra_edit_hint">Username: Nombre de usuario\n… o algún contenido extra</string>
|
||||
<string name="get_last_changed_failed">Error al obtener la fecha de último cambio</string>
|
||||
</resources>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue