diff options
author | Harsh Shandilya <msfjarvis@gmail.com> | 2020-07-23 21:29:04 +0530 |
---|---|---|
committer | Harsh Shandilya <me@msfjarvis.dev> | 2020-07-23 21:38:11 +0530 |
commit | 1546f862c56e72540b79126433e2deb3d26bef7f (patch) | |
tree | 565efee8471e83fedfdbed1b3a86e23fa67a24ba | |
parent | 859da9d9141f15fe6d61a942458a3a10ce89b081 (diff) |
Wire in fallback key selection flow (#958)
Co-authored-by: Fabian Henneke <fabian@henneke.me>
(cherry picked from commit 084b833fa49a583433284f0173cb7342152b263b)
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> |