summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarsh Shandilya <msfjarvis@gmail.com>2020-07-23 21:29:04 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2020-07-23 21:38:11 +0530
commit1546f862c56e72540b79126433e2deb3d26bef7f (patch)
tree565efee8471e83fedfdbed1b3a86e23fa67a24ba
parent859da9d9141f15fe6d61a942458a3a10ce89b081 (diff)
Wire in fallback key selection flow (#958)
Co-authored-by: Fabian Henneke <fabian@henneke.me> (cherry picked from commit 084b833fa49a583433284f0173cb7342152b263b)
-rw-r--r--app/src/main/AndroidManifest.xml5
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt74
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt292
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt3
-rw-r--r--app/src/main/res/values/strings.xml3
5 files changed, 242 insertions, 135 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2fc14f3b..dae4466b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -81,6 +81,11 @@
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"
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt
new file mode 100644
index 00000000..12bc6536
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/GetKeyIdsActivity.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.StartIntentSenderForResult
+import androidx.lifecycle.lifecycleScope
+import com.github.ajalt.timberkt.e
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import me.msfjarvis.openpgpktx.util.OpenPgpApi
+import me.msfjarvis.openpgpktx.util.OpenPgpUtils
+import org.openintents.openpgp.IOpenPgpService2
+
+class GetKeyIdsActivity : BasePgpActivity() {
+
+ private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result ->
+ if (result.data == null || result.resultCode == RESULT_CANCELED) {
+ setResult(RESULT_CANCELED, result.data)
+ finish()
+ return@registerForActivityResult
+ }
+ getKeyIds(result.data!!)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ bindToOpenKeychain(this)
+ }
+
+ 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(data: Intent = 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)?.map {
+ OpenPgpUtils.convertKeyIdToHex(it)
+ } ?: emptyList()
+ val keyResult = Intent().putExtra(OpenPgpApi.EXTRA_KEY_IDS, ids.toTypedArray())
+ setResult(RESULT_OK, keyResult)
+ finish()
+ } catch (e: Exception) {
+ e(e)
+ }
+ }
+ OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
+ val sender = getUserInteractionRequestIntent(result)
+ userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build())
+ }
+ OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
index 851dbaa1..3c1757f1 100644
--- a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
@@ -250,7 +250,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
}
@OptIn(ExperimentalUnsignedTypes::class)
- private fun parseGpgIdentifier(identifier: String) : GpgIdentifier? {
+ private fun parseGpgIdentifier(identifier: String): GpgIdentifier? {
// Match long key IDs:
// FF22334455667788 or 0xFF22334455667788
val maybeLongKeyId = identifier.removePrefix("0x").takeIf {
@@ -279,166 +279,196 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
/**
* Encrypts the password and the extra content
*/
- private fun encrypt(receivedIntent: Intent? = null) = 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
- } else if (editName.contains('/')) {
- snackbar(message = resources.getString(R.string.invalid_filename_text))
- return@with
- }
+ private fun encrypt(receivedIntent: Intent? = null) {
+ with(binding) {
+ val editName = filename.text.toString().trim()
+ val editPass = password.text.toString()
+ val editExtra = extraContent.text.toString()
- if (editPass.isEmpty() && editExtra.isEmpty()) {
- snackbar(message = resources.getString(R.string.empty_toast_text))
- return@with
- }
+ if (editName.isEmpty()) {
+ snackbar(message = resources.getString(R.string.file_toast_text))
+ return@with
+ } else if (editName.contains('/')) {
+ snackbar(message = resources.getString(R.string.invalid_filename_text))
+ return@with
+ }
- if (copy) {
- copyPasswordToClipboard(editPass)
- }
+ if (editPass.isEmpty() && editExtra.isEmpty()) {
+ snackbar(message = resources.getString(R.string.empty_toast_text))
+ return@with
+ }
+
+ if (copy) {
+ copyPasswordToClipboard(editPass)
+ }
- val data = receivedIntent ?: Intent()
- data.action = OpenPgpApi.ACTION_ENCRYPT
+ val data = receivedIntent ?: Intent()
+ data.action = OpenPgpApi.ACTION_ENCRYPT
- // pass enters the key ID into `.gpg-id`.
- val repoRoot = PasswordRepository.getRepositoryDirectory(applicationContext)
- val gpgIdentifierFile = File(repoRoot, directory.text.toString()).findTillRoot(".gpg-id", repoRoot)
- if (gpgIdentifierFile == null) {
- snackbar(message = resources.getString(R.string.failed_to_find_key_id))
- return@with
- }
- val gpgIdentifierFileContent = gpgIdentifierFile.useLines { it.firstOrNull() } ?: ""
- when (val identifier = parseGpgIdentifier(gpgIdentifierFileContent)) {
- is GpgIdentifier.KeyId -> data.putExtra(OpenPgpApi.EXTRA_KEY_IDS, arrayOf(identifier.id).toLongArray())
- is GpgIdentifier.UserId -> data.putExtra(OpenPgpApi.EXTRA_USER_IDS, arrayOf(identifier.email))
- null -> {
- snackbar(message = resources.getString(R.string.invalid_gpg_id))
+ // pass enters the key ID into `.gpg-id`.
+ val repoRoot = PasswordRepository.getRepositoryDirectory(applicationContext)
+ val gpgIdentifierFile = File(repoRoot, directory.text.toString()).findTillRoot(".gpg-id", repoRoot)
+ if (gpgIdentifierFile == null) {
+ snackbar(message = resources.getString(R.string.failed_to_find_key_id))
return@with
}
- }
- 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.
- directoryInputLayout.isEnabled -> {
- val editRelativePath = directory.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
+ val gpgIdentifiers = gpgIdentifierFile.readLines()
+ .filter { it.isNotBlank() }
+ .map { line ->
+ parseGpgIdentifier(line) ?: run {
+ snackbar(message = resources.getString(R.string.invalid_gpg_id))
+ return@with
+ }
}
-
- "${passwordDirectory.path}/$editName.gpg"
+ if (gpgIdentifiers.isEmpty()) {
+ registerForActivityResult(StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) {
+ result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds ->
+ gpgIdentifierFile.writeText(keyIds.joinToString("\n"))
+ val repo = PasswordRepository.getRepository(null)
+ if (repo != null) {
+ commitChange(
+ getString(
+ R.string.git_commit_gpg_id,
+ getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
+ )
+ )
+ }
+ encrypt(data)
+ }
+ }
+ }.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java))
+ return@with
+ }
+ val keyIds = gpgIdentifiers.filterIsInstance<GpgIdentifier.KeyId>().map { it.id }.toLongArray()
+ if (keyIds.isNotEmpty()) {
+ data.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIds)
+ }
+ val userIds = gpgIdentifiers.filterIsInstance<GpgIdentifier.UserId>().map { it.email }.toTypedArray()
+ if (userIds.isNotEmpty()) {
+ data.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds)
}
- 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)
- // If we're not editing, this file should not already exist!
- if (!editing && file.exists()) {
- snackbar(message = getString(R.string.password_creation_duplicate_error))
- return@executeApiAsync
- }
+ data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true)
- if (!isInsideRepository(file)) {
- snackbar(message = getString(R.string.message_error_destination_outside_repo))
- return@executeApiAsync
- }
+ 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.
+ directoryInputLayout.isEnabled -> {
+ val editRelativePath = directory.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 {
- file.outputStream().use {
- it.write(outputStream.toByteArray())
+ val file = File(path)
+ // If we're not editing, this file should not already exist!
+ if (!editing && file.exists()) {
+ snackbar(message = getString(R.string.password_creation_duplicate_error))
+ return@executeApiAsync
}
- } catch (e: IOException) {
- e(e) { "Failed to write password file" }
- setResult(RESULT_CANCELED)
- MaterialAlertDialogBuilder(this@PasswordCreationActivity)
- .setTitle(getString(R.string.password_creation_file_fail_title))
- .setMessage(getString(R.string.password_creation_file_write_fail_message))
- .setCancelable(false)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- finish()
- }
- .show()
- return@executeApiAsync
- }
- 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)
- )
- )
+ if (!isInsideRepository(file)) {
+ snackbar(message = getString(R.string.message_error_destination_outside_repo))
+ return@executeApiAsync
}
- }
- if (directoryInputLayout.isVisible && directoryInputLayout.isEnabled && oldFileName != null) {
- val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg")
- if (oldFile.path != file.path && !oldFile.delete()) {
+ 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(R.string.password_creation_file_fail_title)
- .setMessage(getString(R.string.password_creation_file_delete_fail_message, oldFileName))
+ .setTitle(getString(R.string.password_creation_file_fail_title))
+ .setMessage(getString(R.string.password_creation_file_write_fail_message))
.setCancelable(false)
.setPositiveButton(android.R.string.ok) { _, _ ->
finish()
}
.show()
+ return@executeApiAsync
+ }
+
+ 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)
+ )
+ )
+ }
+ }
+
+ if (directoryInputLayout.isVisible && directoryInputLayout.isEnabled && oldFileName != null) {
+ val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg")
+ if (oldFile.path != file.path && !oldFile.delete()) {
+ setResult(RESULT_CANCELED)
+ MaterialAlertDialogBuilder(this@PasswordCreationActivity)
+ .setTitle(R.string.password_creation_file_fail_title)
+ .setMessage(getString(R.string.password_creation_file_delete_fail_message, oldFileName))
+ .setCancelable(false)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ finish()
+ }
+ .show()
+ } else {
+ setResult(RESULT_OK, returnIntent)
+ finish()
+ }
} else {
setResult(RESULT_OK, returnIntent)
finish()
}
- } else {
- setResult(RESULT_OK, returnIntent)
- finish()
- }
- } catch (e: Exception) {
- e(e) { "An Exception occurred" }
+ } catch (e: Exception) {
+ 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)
}
- OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
- val sender = getUserInteractionRequestIntent(result)
- userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build())
- }
- OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
}
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
index 3235c7fc..9eb549da 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
@@ -37,9 +37,6 @@ object PreferenceKeys {
const val GIT_SERVER_INFO = "git_server_info"
const val HTTPS_PASSWORD = "https_password"
const val LENGTH = "length"
- const val OPENPGP_KEY_IDS_SET = "openpgp_key_ids_set"
- const val OPENPGP_KEY_ID_PREF = "openpgp_key_id_pref"
- const val OPENPGP_PROVIDER_LIST = "openpgp_provider_list"
const val OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES = "oreo_autofill_custom_public_suffixes"
const val OREO_AUTOFILL_DEFAULT_USERNAME = "oreo_autofill_default_username"
const val OREO_AUTOFILL_DIRECTORY_STRUCTURE = "oreo_autofill_directory_structure"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 489b0726..a59d5965 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -45,6 +45,7 @@
<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>
<string name="git_commit_move_multiple_text">Move multiple passwords to %1$s.</string>
+ <string name="git_commit_gpg_id">Initialize GPG IDs in %1$s.</string>
<!-- PGPHandler -->
<string name="clipboard_password_toast_text">Password copied to clipboard, you have %d seconds to paste it somewhere.</string>
@@ -366,7 +367,7 @@
<string name="otp_import_failure">Failed to import TOTP configuration</string>
<string name="exporting_passwords">Exporting passwords…</string>
<string name="failed_to_find_key_id">Failed to locate .gpg-id, is your store set up correctly?</string>
- <string name="invalid_gpg_id">Found .gpg-id, but it did not contain a key ID, fingerprint or user ID</string>
+ <string name="invalid_gpg_id">Found .gpg-id, but it contains an invalid key ID, fingerprint or user ID</string>
<string name="invalid_filename_text">File name must not contain \'/\', set directory above</string>
<string name="directory_hint">Directory</string>
</resources>