From 5dac84c3c882ee42bc541042684de18d7166fdd3 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Thu, 15 Jun 2023 15:49:32 +0530 Subject: refactor: consistently adopt PGP over GPG for naming PGP is the standard, GPG is an implementation of it. We're adhering to PGP, and not using GPG. --- .../passwordstore/data/crypto/CryptoRepository.kt | 10 +- .../data/crypto/GPGPassphraseCache.kt | 80 ------- .../data/crypto/PGPPassphraseCache.kt | 79 +++++++ .../passwordstore/data/password/PasswordItem.kt | 4 +- .../ui/autofill/AutofillDecryptActivity.kt | 16 +- .../app/passwordstore/ui/crypto/BasePGPActivity.kt | 236 +++++++++++++++++++++ .../app/passwordstore/ui/crypto/BasePgpActivity.kt | 236 --------------------- .../app/passwordstore/ui/crypto/DecryptActivity.kt | 20 +- .../ui/crypto/PasswordCreationActivity.kt | 4 +- .../app/passwordstore/ui/main/LaunchActivity.kt | 10 +- .../passwordstore/ui/passwords/PasswordStore.kt | 8 +- .../java/app/passwordstore/ui/pgp/PGPKeyList.kt | 22 +- .../app/passwordstore/ui/settings/PGPSettings.kt | 2 +- .../app/passwordstore/util/features/Feature.kt | 4 +- .../util/viewmodel/PGPKeyListViewModel.kt | 6 +- 15 files changed, 368 insertions(+), 369 deletions(-) delete mode 100644 app/src/main/java/app/passwordstore/data/crypto/GPGPassphraseCache.kt create mode 100644 app/src/main/java/app/passwordstore/data/crypto/PGPPassphraseCache.kt create mode 100644 app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt (limited to 'app') diff --git a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt index ea1d9c76..0ce4b3e3 100644 --- a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt +++ b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt @@ -6,9 +6,9 @@ package app.passwordstore.data.crypto import android.content.SharedPreferences -import app.passwordstore.crypto.GpgIdentifier import app.passwordstore.crypto.PGPDecryptOptions import app.passwordstore.crypto.PGPEncryptOptions +import app.passwordstore.crypto.PGPIdentifier import app.passwordstore.crypto.PGPKeyManager import app.passwordstore.crypto.PGPainlessCryptoHandler import app.passwordstore.crypto.errors.CryptoHandlerException @@ -40,20 +40,20 @@ constructor( suspend fun decrypt( password: String, - identities: List, + identities: List, message: ByteArrayInputStream, out: ByteArrayOutputStream, ) = withContext(dispatcherProvider.io()) { decryptPgp(password, identities, message, out) } suspend fun encrypt( - identities: List, + identities: List, content: ByteArrayInputStream, out: ByteArrayOutputStream, ) = withContext(dispatcherProvider.io()) { encryptPgp(identities, content, out) } private suspend fun decryptPgp( password: String, - identities: List, + identities: List, message: ByteArrayInputStream, out: ByteArrayOutputStream, ): Result { @@ -63,7 +63,7 @@ constructor( } private suspend fun encryptPgp( - identities: List, + identities: List, content: ByteArrayInputStream, out: ByteArrayOutputStream, ): Result { diff --git a/app/src/main/java/app/passwordstore/data/crypto/GPGPassphraseCache.kt b/app/src/main/java/app/passwordstore/data/crypto/GPGPassphraseCache.kt deleted file mode 100644 index e00809a8..00000000 --- a/app/src/main/java/app/passwordstore/data/crypto/GPGPassphraseCache.kt +++ /dev/null @@ -1,80 +0,0 @@ -package app.passwordstore.data.crypto - -import android.content.Context -import androidx.core.content.edit -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import app.passwordstore.crypto.GpgIdentifier -import app.passwordstore.util.coroutines.DispatcherProvider -import app.passwordstore.util.extensions.getString -import javax.inject.Inject -import kotlinx.coroutines.withContext - -/** Implements a rudimentary [EncryptedSharedPreferences]-backed cache for GPG passphrases. */ -@Suppress("Unused") // Soon -class GPGPassphraseCache -@Inject -constructor( - private val dispatcherProvider: DispatcherProvider, -) { - - suspend fun cachePassphrase( - context: Context, - identifier: GpgIdentifier, - passphrase: String, - ) { - withContext(dispatcherProvider.io()) { - getPreferences(context).edit { putString(identifier.toString(), passphrase) } - } - } - - suspend fun retrieveCachedPassphrase( - context: Context, - identifier: GpgIdentifier, - ): String? { - return withContext(dispatcherProvider.io()) { - getPreferences(context).getString(identifier.toString()) - } - } - - suspend fun clearCachedPassphrase( - context: Context, - identifier: GpgIdentifier, - ) { - withContext(dispatcherProvider.io()) { - getPreferences(context).edit { remove(identifier.toString()) } - } - } - - suspend fun clearAllCachedPassphrases(context: Context) { - withContext(dispatcherProvider.io()) { getPreferences(context).edit { clear() } } - } - - private suspend fun getPreferences(context: Context) = - withContext(dispatcherProvider.io()) { - EncryptedSharedPreferences.create( - context, - ANDROIDX_SECURITY_KEYSET_PREF_NAME, - getOrCreateWrappingMasterKey(context), - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, - ) - } - - private suspend fun getOrCreateWrappingMasterKey(context: Context) = - withContext(dispatcherProvider.io()) { - MasterKey.Builder(context, "passphrase") - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .setRequestStrongBoxBacked(true) - .setUserAuthenticationRequired( - /* authenticationRequired = */ true, - /* userAuthenticationValidityDurationSeconds = */ 60, - ) - .build() - } - - private companion object { - - private const val ANDROIDX_SECURITY_KEYSET_PREF_NAME = "androidx_passphrase_keyset_prefs" - } -} diff --git a/app/src/main/java/app/passwordstore/data/crypto/PGPPassphraseCache.kt b/app/src/main/java/app/passwordstore/data/crypto/PGPPassphraseCache.kt new file mode 100644 index 00000000..0632e3c8 --- /dev/null +++ b/app/src/main/java/app/passwordstore/data/crypto/PGPPassphraseCache.kt @@ -0,0 +1,79 @@ +package app.passwordstore.data.crypto + +import android.content.Context +import androidx.core.content.edit +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import app.passwordstore.crypto.PGPIdentifier +import app.passwordstore.util.coroutines.DispatcherProvider +import app.passwordstore.util.extensions.getString +import javax.inject.Inject +import kotlinx.coroutines.withContext + +/** Implements a rudimentary [EncryptedSharedPreferences]-backed cache for GPG passphrases. */ +class PGPPassphraseCache +@Inject +constructor( + private val dispatcherProvider: DispatcherProvider, +) { + + suspend fun cachePassphrase( + context: Context, + identifier: PGPIdentifier, + passphrase: String, + ) { + withContext(dispatcherProvider.io()) { + getPreferences(context).edit { putString(identifier.toString(), passphrase) } + } + } + + suspend fun retrieveCachedPassphrase( + context: Context, + identifier: PGPIdentifier, + ): String? { + return withContext(dispatcherProvider.io()) { + getPreferences(context).getString(identifier.toString()) + } + } + + suspend fun clearCachedPassphrase( + context: Context, + identifier: PGPIdentifier, + ) { + withContext(dispatcherProvider.io()) { + getPreferences(context).edit { remove(identifier.toString()) } + } + } + + suspend fun clearAllCachedPassphrases(context: Context) { + withContext(dispatcherProvider.io()) { getPreferences(context).edit { clear() } } + } + + private suspend fun getPreferences(context: Context) = + withContext(dispatcherProvider.io()) { + EncryptedSharedPreferences.create( + context, + ANDROIDX_SECURITY_KEYSET_PREF_NAME, + getOrCreateWrappingMasterKey(context), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + } + + private suspend fun getOrCreateWrappingMasterKey(context: Context) = + withContext(dispatcherProvider.io()) { + MasterKey.Builder(context, "passphrase") + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .setRequestStrongBoxBacked(true) + .setUserAuthenticationRequired( + /* authenticationRequired = */ true, + /* userAuthenticationValidityDurationSeconds = */ 60, + ) + .build() + } + + private companion object { + + private const val ANDROIDX_SECURITY_KEYSET_PREF_NAME = "androidx_passphrase_keyset_prefs" + } +} diff --git a/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt b/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt index 77497671..be3443ef 100644 --- a/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt +++ b/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt @@ -7,7 +7,7 @@ package app.passwordstore.data.password import android.content.Context import android.content.Intent import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.ui.crypto.BasePgpActivity +import app.passwordstore.ui.crypto.BasePGPActivity import app.passwordstore.ui.main.LaunchActivity import java.io.File @@ -21,7 +21,7 @@ data class PasswordItem( val fullPathToParent = file.absolutePath.replace(rootDir.absolutePath, "").replace(file.name, "") - val longName = BasePgpActivity.getLongName(fullPathToParent, rootDir.absolutePath, toString()) + val longName = BasePGPActivity.getLongName(fullPathToParent, rootDir.absolutePath, toString()) override fun equals(other: Any?): Boolean { return (other is PasswordItem) && (other.file == file) diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt index 959728cd..8d0cf28a 100644 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt @@ -13,16 +13,16 @@ import android.os.Bundle import android.view.autofill.AutofillManager import androidx.lifecycle.lifecycleScope import app.passwordstore.R -import app.passwordstore.data.crypto.GPGPassphraseCache +import app.passwordstore.data.crypto.PGPPassphraseCache import app.passwordstore.data.passfile.PasswordEntry -import app.passwordstore.ui.crypto.BasePgpActivity +import app.passwordstore.ui.crypto.BasePGPActivity import app.passwordstore.ui.crypto.PasswordDialog import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.autofill.AutofillPreferences import app.passwordstore.util.autofill.AutofillResponseBuilder import app.passwordstore.util.autofill.DirectoryStructure import app.passwordstore.util.extensions.asLog -import app.passwordstore.util.features.Feature.EnableGPGPassphraseCache +import app.passwordstore.util.features.Feature.EnablePGPPassphraseCache import app.passwordstore.util.features.Features import com.github.androidpasswordstore.autofillparser.AutofillAction import com.github.androidpasswordstore.autofillparser.Credentials @@ -41,11 +41,11 @@ import logcat.LogPriority.ERROR import logcat.logcat @AndroidEntryPoint -class AutofillDecryptActivity : BasePgpActivity() { +class AutofillDecryptActivity : BasePGPActivity() { @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory @Inject lateinit var features: Features - @Inject lateinit var passphraseCache: GPGPassphraseCache + @Inject lateinit var passphraseCache: PGPPassphraseCache private lateinit var directoryStructure: DirectoryStructure @@ -70,9 +70,9 @@ class AutofillDecryptActivity : BasePgpActivity() { directoryStructure = AutofillPreferences.directoryStructure(this) logcat { action.toString() } requireKeysExist { - val gpgIdentifiers = getGpgIdentifiers("") ?: return@requireKeysExist + val gpgIdentifiers = getPGPIdentifiers("") ?: return@requireKeysExist if ( - features.isEnabled(EnableGPGPassphraseCache) && BiometricAuthenticator.canAuthenticate(this) + features.isEnabled(EnablePGPPassphraseCache) && BiometricAuthenticator.canAuthenticate(this) ) { BiometricAuthenticator.authenticate( this, @@ -143,7 +143,7 @@ class AutofillDecryptActivity : BasePgpActivity() { } private suspend fun decryptCredential(file: File, password: String): Credentials? { - val gpgIdentifiers = getGpgIdentifiers("") ?: return null + val gpgIdentifiers = getPGPIdentifiers("") ?: return null runCatching { file.readBytes().inputStream() } .onFailure { e -> logcat(ERROR) { e.asLog("File to decrypt not found") } diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt new file mode 100644 index 00000000..d0039d52 --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt @@ -0,0 +1,236 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.crypto + +import android.content.ClipData +import android.content.Intent +import android.content.SharedPreferences +import android.os.Build +import android.os.Bundle +import android.os.PersistableBundle +import android.view.WindowManager +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.annotation.CallSuper +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import app.passwordstore.R +import app.passwordstore.crypto.PGPIdentifier +import app.passwordstore.data.crypto.CryptoRepository +import app.passwordstore.data.repo.PasswordRepository +import app.passwordstore.injection.prefs.SettingsPreferences +import app.passwordstore.ui.pgp.PGPKeyImportActivity +import app.passwordstore.util.coroutines.DispatcherProvider +import app.passwordstore.util.extensions.clipboard +import app.passwordstore.util.extensions.getString +import app.passwordstore.util.extensions.snackbar +import app.passwordstore.util.extensions.unsafeLazy +import app.passwordstore.util.services.ClipboardService +import app.passwordstore.util.settings.Constants +import app.passwordstore.util.settings.PreferenceKeys +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint +import java.io.File +import javax.inject.Inject +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Suppress("Registered") +@AndroidEntryPoint +open class BasePGPActivity : AppCompatActivity() { + + /** Full path to the repository */ + val repoPath by unsafeLazy { intent.getStringExtra("REPO_PATH")!! } + + /** Full path to the password file being worked on */ + val fullPath by unsafeLazy { 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 unsafeLazy { File(fullPath).nameWithoutExtension } + + /** Action to invoke if [keyImportAction] succeeds. */ + private var onKeyImport: (() -> Unit)? = null + private val keyImportAction = + registerForActivityResult(StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + onKeyImport?.invoke() + onKeyImport = null + } else { + finish() + } + } + + /** [SharedPreferences] instance used by subclasses to persist settings */ + @SettingsPreferences @Inject lateinit var settings: SharedPreferences + @Inject lateinit var repository: CryptoRepository + @Inject lateinit var dispatcherProvider: DispatcherProvider + + /** + * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots or + * recent apps screen. + */ + @CallSuper + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + } + + /** + * 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, + @StringRes snackbarTextRes: Int = R.string.clipboard_copied_text + ) { + val clipboard = clipboard ?: return + val clip = ClipData.newPlainText("pgp_handler_result_pm", text) + clip.description.extras = + PersistableBundle().apply { putBoolean("android.content.extra.IS_SENSITIVE", true) } + clipboard.setPrimaryClip(clip) + if (showSnackbar && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + snackbar(message = resources.getString(snackbarTextRes)) + } + } + + /** + * Function to execute [onKeysExist] only if there are PGP keys imported in the app's key manager. + */ + fun requireKeysExist(onKeysExist: () -> Unit) { + lifecycleScope.launch { + val hasKeys = repository.hasKeys() + if (!hasKeys) { + withContext(dispatcherProvider.main()) { + MaterialAlertDialogBuilder(this@BasePGPActivity) + .setTitle(resources.getString(R.string.no_keys_imported_dialog_title)) + .setMessage(resources.getString(R.string.no_keys_imported_dialog_message)) + .setPositiveButton(resources.getString(R.string.button_label_import)) { _, _ -> + onKeyImport = onKeysExist + keyImportAction.launch(Intent(this@BasePGPActivity, PGPKeyImportActivity::class.java)) + } + .show() + } + } else { + onKeysExist() + } + } + } + + /** + * 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) + val clearAfter = + settings.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() + ?: Constants.DEFAULT_DECRYPTION_TIMEOUT + + if (clearAfter != 0) { + val service = + Intent(this, ClipboardService::class.java).apply { + action = ClipboardService.ACTION_START + putExtra(ClipboardService.EXTRA_NOTIFICATION_TIME, clearAfter) + } + startForegroundService(service) + } + } + + /** + * Get a list of available [PGPIdentifier]s for the current password repository. This method + * throws when no identifiers were able to be parsed. If this method returns null, it means that + * an invalid identifier was encountered and further execution must stop to let the user correct + * the problem. + */ + fun getPGPIdentifiers(subDir: String): List? { + val repoRoot = PasswordRepository.getRepositoryDirectory() + val gpgIdentifierFile = + File(repoRoot, subDir).findTillRoot(".gpg-id", repoRoot) + ?: File(repoRoot, ".gpg-id").apply { createNewFile() } + val gpgIdentifiers = + gpgIdentifierFile + .readLines() + .filter { it.isNotBlank() } + .map { line -> + PGPIdentifier.fromString(line) + ?: run { + // The line being empty means this is most likely an empty `.gpg-id` + // file we created. Skip the validation so we can make the user add a + // real ID. + if (line.isEmpty()) return@run + // Apparently `gpg-id` being the first line is also acceptable? + if (line == "gpg-id") return@run + if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex()).not()) { + snackbar(message = resources.getString(R.string.invalid_gpg_id)) + } + return null + } + } + .filterIsInstance() + if (gpgIdentifiers.isEmpty()) { + error("Failed to parse identifiers from .gpg-id: ${gpgIdentifierFile.readText()}") + } + return gpgIdentifiers + } + + @Suppress("ReturnCount") + private fun File.findTillRoot(fileName: String, rootPath: File): File? { + val gpgFile = File(this, fileName) + if (gpgFile.exists()) return gpgFile + + if (this.absolutePath == rootPath.absolutePath) { + return null + } + val parent = parentFile + return if (parent != null && parent.exists()) { + parent.findTillRoot(fileName, rootPath) + } else { + null + } + } + + companion object { + + const val EXTRA_FILE_PATH = "FILE_PATH" + const val EXTRA_REPO_PATH = "REPO_PATH" + + /** Gets the relative path to the repository */ + fun getRelativePath(fullPath: String, repositoryPath: String): String = + fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") + + /** Gets the Parent path, relative to the repository */ + fun getParentPath(fullPath: String, repositoryPath: String): String { + val relativePath = getRelativePath(fullPath, repositoryPath) + val index = relativePath.lastIndexOf("/") + return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/" + .replace("/+".toRegex(), "/") + } + + /** /path/to/store/social/facebook.gpg -> social/facebook */ + @JvmStatic + fun getLongName(fullPath: String, repositoryPath: String, basename: String): String { + var relativePath = getRelativePath(fullPath, repositoryPath) + return if (relativePath.isNotEmpty() && relativePath != "/") { + // remove preceding '/' + relativePath = relativePath.substring(1) + if (relativePath.endsWith('/')) { + relativePath + basename + } else { + "$relativePath/$basename" + } + } else { + basename + } + } + } +} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt deleted file mode 100644 index 417565a7..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.crypto - -import android.content.ClipData -import android.content.Intent -import android.content.SharedPreferences -import android.os.Build -import android.os.Bundle -import android.os.PersistableBundle -import android.view.WindowManager -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.annotation.CallSuper -import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope -import app.passwordstore.R -import app.passwordstore.crypto.GpgIdentifier -import app.passwordstore.data.crypto.CryptoRepository -import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.injection.prefs.SettingsPreferences -import app.passwordstore.ui.pgp.PGPKeyImportActivity -import app.passwordstore.util.coroutines.DispatcherProvider -import app.passwordstore.util.extensions.clipboard -import app.passwordstore.util.extensions.getString -import app.passwordstore.util.extensions.snackbar -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.services.ClipboardService -import app.passwordstore.util.settings.Constants -import app.passwordstore.util.settings.PreferenceKeys -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import dagger.hilt.android.AndroidEntryPoint -import java.io.File -import javax.inject.Inject -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -@Suppress("Registered") -@AndroidEntryPoint -open class BasePgpActivity : AppCompatActivity() { - - /** Full path to the repository */ - val repoPath by unsafeLazy { intent.getStringExtra("REPO_PATH")!! } - - /** Full path to the password file being worked on */ - val fullPath by unsafeLazy { 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 unsafeLazy { File(fullPath).nameWithoutExtension } - - /** Action to invoke if [keyImportAction] succeeds. */ - private var onKeyImport: (() -> Unit)? = null - private val keyImportAction = - registerForActivityResult(StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - onKeyImport?.invoke() - onKeyImport = null - } else { - finish() - } - } - - /** [SharedPreferences] instance used by subclasses to persist settings */ - @SettingsPreferences @Inject lateinit var settings: SharedPreferences - @Inject lateinit var repository: CryptoRepository - @Inject lateinit var dispatcherProvider: DispatcherProvider - - /** - * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots or - * recent apps screen. - */ - @CallSuper - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) - } - - /** - * 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, - @StringRes snackbarTextRes: Int = R.string.clipboard_copied_text - ) { - val clipboard = clipboard ?: return - val clip = ClipData.newPlainText("pgp_handler_result_pm", text) - clip.description.extras = - PersistableBundle().apply { putBoolean("android.content.extra.IS_SENSITIVE", true) } - clipboard.setPrimaryClip(clip) - if (showSnackbar && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - snackbar(message = resources.getString(snackbarTextRes)) - } - } - - /** - * Function to execute [onKeysExist] only if there are PGP keys imported in the app's key manager. - */ - fun requireKeysExist(onKeysExist: () -> Unit) { - lifecycleScope.launch { - val hasKeys = repository.hasKeys() - if (!hasKeys) { - withContext(dispatcherProvider.main()) { - MaterialAlertDialogBuilder(this@BasePgpActivity) - .setTitle(resources.getString(R.string.no_keys_imported_dialog_title)) - .setMessage(resources.getString(R.string.no_keys_imported_dialog_message)) - .setPositiveButton(resources.getString(R.string.button_label_import)) { _, _ -> - onKeyImport = onKeysExist - keyImportAction.launch(Intent(this@BasePgpActivity, PGPKeyImportActivity::class.java)) - } - .show() - } - } else { - onKeysExist() - } - } - } - - /** - * 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) - val clearAfter = - settings.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() - ?: Constants.DEFAULT_DECRYPTION_TIMEOUT - - if (clearAfter != 0) { - val service = - Intent(this, ClipboardService::class.java).apply { - action = ClipboardService.ACTION_START - putExtra(ClipboardService.EXTRA_NOTIFICATION_TIME, clearAfter) - } - startForegroundService(service) - } - } - - /** - * Get a list of available [GpgIdentifier]s for the current password repository. This method - * throws when no identifiers were able to be parsed. If this method returns null, it means that - * an invalid identifier was encountered and further execution must stop to let the user correct - * the problem. - */ - fun getGpgIdentifiers(subDir: String): List? { - val repoRoot = PasswordRepository.getRepositoryDirectory() - val gpgIdentifierFile = - File(repoRoot, subDir).findTillRoot(".gpg-id", repoRoot) - ?: File(repoRoot, ".gpg-id").apply { createNewFile() } - val gpgIdentifiers = - gpgIdentifierFile - .readLines() - .filter { it.isNotBlank() } - .map { line -> - GpgIdentifier.fromString(line) - ?: run { - // The line being empty means this is most likely an empty `.gpg-id` - // file we created. Skip the validation so we can make the user add a - // real ID. - if (line.isEmpty()) return@run - // Apparently `gpg-id` being the first line is also acceptable? - if (line == "gpg-id") return@run - if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex()).not()) { - snackbar(message = resources.getString(R.string.invalid_gpg_id)) - } - return null - } - } - .filterIsInstance() - if (gpgIdentifiers.isEmpty()) { - error("Failed to parse identifiers from .gpg-id: ${gpgIdentifierFile.readText()}") - } - return gpgIdentifiers - } - - @Suppress("ReturnCount") - private fun File.findTillRoot(fileName: String, rootPath: File): File? { - val gpgFile = File(this, fileName) - if (gpgFile.exists()) return gpgFile - - if (this.absolutePath == rootPath.absolutePath) { - return null - } - val parent = parentFile - return if (parent != null && parent.exists()) { - parent.findTillRoot(fileName, rootPath) - } else { - null - } - } - - companion object { - - const val EXTRA_FILE_PATH = "FILE_PATH" - const val EXTRA_REPO_PATH = "REPO_PATH" - - /** Gets the relative path to the repository */ - fun getRelativePath(fullPath: String, repositoryPath: String): String = - fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") - - /** Gets the Parent path, relative to the repository */ - fun getParentPath(fullPath: String, repositoryPath: String): String { - val relativePath = getRelativePath(fullPath, repositoryPath) - val index = relativePath.lastIndexOf("/") - return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/" - .replace("/+".toRegex(), "/") - } - - /** /path/to/store/social/facebook.gpg -> social/facebook */ - @JvmStatic - fun getLongName(fullPath: String, repositoryPath: String, basename: String): String { - var relativePath = getRelativePath(fullPath, repositoryPath) - return if (relativePath.isNotEmpty() && relativePath != "/") { - // remove preceding '/' - relativePath = relativePath.substring(1) - if (relativePath.endsWith('/')) { - relativePath + basename - } else { - "$relativePath/$basename" - } - } else { - basename - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt index 552332f1..ca6cd257 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt @@ -11,8 +11,8 @@ import android.view.Menu import android.view.MenuItem import androidx.lifecycle.lifecycleScope import app.passwordstore.R -import app.passwordstore.crypto.GpgIdentifier -import app.passwordstore.data.crypto.GPGPassphraseCache +import app.passwordstore.crypto.PGPIdentifier +import app.passwordstore.data.crypto.PGPPassphraseCache import app.passwordstore.data.passfile.PasswordEntry import app.passwordstore.data.password.FieldItem import app.passwordstore.databinding.DecryptLayoutBinding @@ -21,7 +21,7 @@ import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.extensions.getString import app.passwordstore.util.extensions.unsafeLazy import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.features.Feature.EnableGPGPassphraseCache +import app.passwordstore.util.features.Feature.EnablePGPPassphraseCache import app.passwordstore.util.features.Features import app.passwordstore.util.settings.Constants import app.passwordstore.util.settings.PreferenceKeys @@ -42,10 +42,10 @@ import logcat.LogPriority.ERROR import logcat.logcat @AndroidEntryPoint -class DecryptActivity : BasePgpActivity() { +class DecryptActivity : BasePGPActivity() { @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var passphraseCache: GPGPassphraseCache + @Inject lateinit var passphraseCache: PGPPassphraseCache @Inject lateinit var features: Features private val binding by viewBinding(DecryptLayoutBinding::inflate) @@ -67,7 +67,7 @@ class DecryptActivity : BasePgpActivity() { } } if ( - features.isEnabled(EnableGPGPassphraseCache) && + features.isEnabled(EnablePGPPassphraseCache) && BiometricAuthenticator.canAuthenticate(this@DecryptActivity) ) { BiometricAuthenticator.authenticate( @@ -150,7 +150,7 @@ class DecryptActivity : BasePgpActivity() { } private fun decrypt(isError: Boolean, authResult: BiometricAuthenticator.Result) { - val gpgIdentifiers = getGpgIdentifiers("") ?: return + val gpgIdentifiers = getPGPIdentifiers("") ?: return lifecycleScope.launch(dispatcherProvider.main()) { if (authResult is BiometricAuthenticator.Result.Success) { val cachedPassphrase = @@ -168,7 +168,7 @@ class DecryptActivity : BasePgpActivity() { private fun askPassphrase( isError: Boolean, - gpgIdentifiers: List, + gpgIdentifiers: List, authResult: BiometricAuthenticator.Result, ) { if (retries < MAX_RETRIES) { @@ -206,7 +206,7 @@ class DecryptActivity : BasePgpActivity() { private suspend fun decryptWithCachedPassphrase( passphrase: String, - identifiers: List, + identifiers: List, authResult: BiometricAuthenticator.Result, ) { when (val result = decryptWithPassphrase(passphrase, identifiers)) { @@ -225,7 +225,7 @@ class DecryptActivity : BasePgpActivity() { private suspend fun decryptWithPassphrase( password: String, - gpgIdentifiers: List, + gpgIdentifiers: List, ) = runCatching { val message = withContext(dispatcherProvider.io()) { File(fullPath).readBytes().inputStream() } val outputStream = ByteArrayOutputStream() diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt index f724ec3c..10677091 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt @@ -63,7 +63,7 @@ import logcat.asLog import logcat.logcat @AndroidEntryPoint -class PasswordCreationActivity : BasePgpActivity() { +class PasswordCreationActivity : BasePGPActivity() { private val binding by viewBinding(PasswordCreationActivityBinding::inflate) @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory @@ -330,7 +330,7 @@ class PasswordCreationActivity : BasePgpActivity() { } // pass enters the key ID into `.gpg-id`. - val gpgIdentifiers = getGpgIdentifiers(directory.text.toString()) ?: return@with + val gpgIdentifiers = getPGPIdentifiers(directory.text.toString()) ?: return@with val content = "$editPass\n$editExtra" val path = when { diff --git a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt index 8e7f52c5..15cd77ad 100644 --- a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt @@ -11,7 +11,7 @@ import android.os.Handler import android.os.Looper import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit -import app.passwordstore.ui.crypto.BasePgpActivity +import app.passwordstore.ui.crypto.BasePGPActivity import app.passwordstore.ui.crypto.DecryptActivity import app.passwordstore.ui.passwords.PasswordStore import app.passwordstore.util.auth.BiometricAuthenticator @@ -56,12 +56,12 @@ class LaunchActivity : AppCompatActivity() { if (intent.action == ACTION_DECRYPT_PASS) getDecryptIntent().apply { putExtra( - BasePgpActivity.EXTRA_FILE_PATH, - intent.getStringExtra(BasePgpActivity.EXTRA_FILE_PATH) + BasePGPActivity.EXTRA_FILE_PATH, + intent.getStringExtra(BasePGPActivity.EXTRA_FILE_PATH) ) putExtra( - BasePgpActivity.EXTRA_REPO_PATH, - intent.getStringExtra(BasePgpActivity.EXTRA_REPO_PATH) + BasePGPActivity.EXTRA_REPO_PATH, + intent.getStringExtra(BasePGPActivity.EXTRA_REPO_PATH) ) } else Intent(this, PasswordStore::class.java) diff --git a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt index 80b08b77..76e2702c 100644 --- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt +++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt @@ -25,8 +25,8 @@ import androidx.lifecycle.lifecycleScope import app.passwordstore.R import app.passwordstore.data.password.PasswordItem import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.ui.crypto.BasePgpActivity -import app.passwordstore.ui.crypto.BasePgpActivity.Companion.getLongName +import app.passwordstore.ui.crypto.BasePGPActivity +import app.passwordstore.ui.crypto.BasePGPActivity.Companion.getLongName import app.passwordstore.ui.crypto.DecryptActivity import app.passwordstore.ui.crypto.PasswordCreationActivity import app.passwordstore.ui.dialogs.FolderCreationDialogFragment @@ -393,9 +393,9 @@ class PasswordStore : BaseGitActivity() { val currentDir = currentDir logcat(INFO) { "Adding file to : ${currentDir.absolutePath}" } val intent = Intent(this, PasswordCreationActivity::class.java) - intent.putExtra(BasePgpActivity.EXTRA_FILE_PATH, currentDir.absolutePath) + intent.putExtra(BasePGPActivity.EXTRA_FILE_PATH, currentDir.absolutePath) intent.putExtra( - BasePgpActivity.EXTRA_REPO_PATH, + BasePGPActivity.EXTRA_REPO_PATH, PasswordRepository.getRepositoryDirectory().absolutePath ) listRefreshAction.launch(intent) diff --git a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt index 04a57635..40a8654d 100644 --- a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt +++ b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import app.passwordstore.R -import app.passwordstore.crypto.GpgIdentifier +import app.passwordstore.crypto.PGPIdentifier import app.passwordstore.ui.compose.theme.APSThemePreview import app.passwordstore.util.extensions.conditional import kotlinx.collections.immutable.ImmutableList @@ -41,10 +41,10 @@ import kotlinx.collections.immutable.toPersistentList @Composable fun KeyList( - identifiers: ImmutableList, - onItemClick: (identifier: GpgIdentifier) -> Unit, + identifiers: ImmutableList, + onItemClick: (identifier: PGPIdentifier) -> Unit, modifier: Modifier = Modifier, - onKeySelected: ((identifier: GpgIdentifier) -> Unit)? = null, + onKeySelected: ((identifier: PGPIdentifier) -> Unit)? = null, ) { if (identifiers.isEmpty()) { Column( @@ -69,10 +69,10 @@ fun KeyList( @Composable private fun KeyItem( - identifier: GpgIdentifier, - onItemClick: (identifier: GpgIdentifier) -> Unit, + identifier: PGPIdentifier, + onItemClick: (identifier: PGPIdentifier) -> Unit, modifier: Modifier = Modifier, - onKeySelected: ((identifier: GpgIdentifier) -> Unit)? = null, + onKeySelected: ((identifier: PGPIdentifier) -> Unit)? = null, ) { var isDeleting by remember { mutableStateOf(false) } DeleteConfirmationDialog( @@ -85,8 +85,8 @@ private fun KeyItem( ) val label = when (identifier) { - is GpgIdentifier.KeyId -> identifier.id.toString() - is GpgIdentifier.UserId -> identifier.email + is PGPIdentifier.KeyId -> identifier.id.toString() + is PGPIdentifier.UserId -> identifier.email } Row( modifier = @@ -144,8 +144,8 @@ private fun KeyListPreview() { KeyList( identifiers = listOfNotNull( - GpgIdentifier.fromString("ultramicroscopicsilicovolcanoconiosis@example.com"), - GpgIdentifier.fromString("0xB950AE2813841585"), + PGPIdentifier.fromString("ultramicroscopicsilicovolcanoconiosis@example.com"), + PGPIdentifier.fromString("0xB950AE2813841585"), ) .toPersistentList(), onItemClick = {} diff --git a/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt index e3c3e90c..c5625612 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt @@ -32,7 +32,7 @@ class PGPSettings(private val activity: FragmentActivity) : SettingsProvider { titleRes = R.string.pref_pgp_ascii_armor_title persistent = true } - switch(Feature.EnableGPGPassphraseCache.configKey) { + switch(Feature.EnablePGPPassphraseCache.configKey) { titleRes = R.string.pref_passphrase_cache_title summaryRes = R.string.pref_passphrase_cache_summary defaultValue = false diff --git a/app/src/main/java/app/passwordstore/util/features/Feature.kt b/app/src/main/java/app/passwordstore/util/features/Feature.kt index a0729f4d..55e4eaeb 100644 --- a/app/src/main/java/app/passwordstore/util/features/Feature.kt +++ b/app/src/main/java/app/passwordstore/util/features/Feature.kt @@ -23,8 +23,8 @@ enum class Feature( /** Opt into the new SSH layer implemented as a freestanding module. */ EnableNewSSHLayer(false, "enable_new_ssh"), - /** Opt into a cache layer for GPG passphrases. */ - EnableGPGPassphraseCache(false, "enable_gpg_passphrase_cache"), + /** Opt into a cache layer for PGP passphrases. */ + EnablePGPPassphraseCache(false, "enable_gpg_passphrase_cache"), ; companion object { diff --git a/app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt b/app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt index a08b41fa..6711cae1 100644 --- a/app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt +++ b/app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt @@ -5,8 +5,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.passwordstore.crypto.GpgIdentifier import app.passwordstore.crypto.KeyUtils +import app.passwordstore.crypto.PGPIdentifier import app.passwordstore.crypto.PGPKeyManager import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch @HiltViewModel class PGPKeyListViewModel @Inject constructor(private val keyManager: PGPKeyManager) : ViewModel() { - var keys: ImmutableList by mutableStateOf(persistentListOf()) + var keys: ImmutableList by mutableStateOf(persistentListOf()) init { updateKeySet() @@ -40,7 +40,7 @@ class PGPKeyListViewModel @Inject constructor(private val keyManager: PGPKeyMana } } - fun deleteKey(identifier: GpgIdentifier) { + fun deleteKey(identifier: PGPIdentifier) { viewModelScope.launch { keyManager.removeKey(identifier) updateKeySet() -- cgit v1.2.3