diff options
Diffstat (limited to 'app/src/main/java')
5 files changed, 0 insertions, 589 deletions
diff --git a/app/src/main/java/app/passwordstore/data/password/FieldItem.kt b/app/src/main/java/app/passwordstore/data/password/FieldItem.kt deleted file mode 100644 index 56ee8e0e..00000000 --- a/app/src/main/java/app/passwordstore/data/password/FieldItem.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.data.password - -import app.passwordstore.data.passfile.Totp - -class FieldItem -private constructor( - val type: ItemType, - val label: String, - val value: String, - val action: ActionType, -) { - enum class ActionType { - COPY, - HIDE, - } - - enum class ItemType() { - USERNAME, - PASSWORD, - OTP, - FREEFORM, - } - - companion object { - fun createOtpField(label: String, totp: Totp): FieldItem { - return FieldItem( - ItemType.OTP, - label.format(totp.remainingTime.inWholeSeconds), - totp.value, - ActionType.COPY, - ) - } - - fun createPasswordField(label: String, password: String): FieldItem { - return FieldItem(ItemType.PASSWORD, label, password, ActionType.HIDE) - } - - fun createUsernameField(label: String, username: String): FieldItem { - return FieldItem(ItemType.USERNAME, label, username, ActionType.COPY) - } - - fun createFreeformField(label: String, content: String): FieldItem { - return FieldItem(ItemType.FREEFORM, label, content, ActionType.COPY) - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/adapters/FieldItemAdapter.kt b/app/src/main/java/app/passwordstore/ui/adapters/FieldItemAdapter.kt deleted file mode 100644 index 57428ffb..00000000 --- a/app/src/main/java/app/passwordstore/ui/adapters/FieldItemAdapter.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.adapters - -import android.text.method.PasswordTransformationMethod -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat -import androidx.recyclerview.widget.RecyclerView -import app.passwordstore.R -import app.passwordstore.data.passfile.Totp -import app.passwordstore.data.password.FieldItem -import app.passwordstore.databinding.ItemFieldBinding -import app.passwordstore.ui.compose.R as composeR -import com.google.android.material.textfield.TextInputLayout - -class FieldItemAdapter( - private var fieldItemList: List<FieldItem>, - private val showPassword: Boolean, - private val copyTextToClipboard: (text: String?) -> Unit, -) : RecyclerView.Adapter<FieldItemAdapter.FieldItemViewHolder>() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldItemViewHolder { - val binding = ItemFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return FieldItemViewHolder(binding.root, binding) - } - - override fun onBindViewHolder(holder: FieldItemViewHolder, position: Int) { - holder.bind(fieldItemList[position], showPassword, copyTextToClipboard) - } - - override fun getItemCount(): Int { - return fieldItemList.size - } - - fun updateOTPCode(totp: Totp, labelFormat: String) { - var otpItemPosition = -1 - fieldItemList = - fieldItemList.mapIndexed { position, item -> - if (item.type == FieldItem.ItemType.OTP) { - otpItemPosition = position - return@mapIndexed FieldItem.createOtpField(labelFormat, totp) - } - - return@mapIndexed item - } - - notifyItemChanged(otpItemPosition) - } - - class FieldItemViewHolder(itemView: View, val binding: ItemFieldBinding) : - RecyclerView.ViewHolder(itemView) { - - fun bind(fieldItem: FieldItem, showPassword: Boolean, copyTextToClipboard: (String?) -> Unit) { - with(binding) { - itemText.hint = fieldItem.label - itemTextContainer.hint = fieldItem.label - itemText.setText(fieldItem.value) - - when (fieldItem.action) { - FieldItem.ActionType.COPY -> { - itemTextContainer.apply { - endIconDrawable = - ContextCompat.getDrawable(itemView.context, R.drawable.ic_content_copy) - endIconMode = TextInputLayout.END_ICON_CUSTOM - setEndIconOnClickListener { copyTextToClipboard(itemText.text.toString()) } - } - itemText.transformationMethod = null - } - FieldItem.ActionType.HIDE -> { - itemTextContainer.apply { - endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE - setOnClickListener { copyTextToClipboard(itemText.text.toString()) } - } - itemText.apply { - transformationMethod = - if (!showPassword) { - PasswordTransformationMethod.getInstance() - } else { - null - } - if (fieldItem.type == FieldItem.ItemType.PASSWORD) { - typeface = - ResourcesCompat.getFont( - binding.root.context, - composeR.font.jetbrainsmono_nl_regular, - ) - } - setOnClickListener { copyTextToClipboard(itemText.text.toString()) } - } - } - } - } - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt index 95dc67ad..ba49a62c 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt @@ -50,13 +50,6 @@ open class BasePGPActivity : AppCompatActivity() { /** 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 = diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt deleted file mode 100644 index 8b2109bf..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.crypto - -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import androidx.core.content.edit -import androidx.fragment.app.setFragmentResultListener -import androidx.lifecycle.lifecycleScope -import app.passwordstore.Application.Companion.screenWasOff -import app.passwordstore.R -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 -import app.passwordstore.ui.adapters.FieldItemAdapter -import app.passwordstore.util.auth.BiometricAuthenticator -import app.passwordstore.util.auth.BiometricAuthenticator.Result as BiometricResult -import app.passwordstore.util.extensions.getString -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.extensions.viewBinding -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 -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import dagger.hilt.android.AndroidEntryPoint -import java.io.ByteArrayOutputStream -import java.io.File -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat - -@AndroidEntryPoint -class DecryptActivity : BasePGPActivity() { - - @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var passphraseCache: PGPPassphraseCache - @Inject lateinit var features: Features - - private val binding by viewBinding(DecryptLayoutBinding::inflate) - private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) } - private var passwordEntry: PasswordEntry? = null - private var retries = 0 - private var clearCache = true - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - title = name - with(binding) { - setContentView(root) - passwordCategory.text = relativeParentPath - passwordFile.text = name - passwordFile.setOnLongClickListener { - copyTextToClipboard(name) - true - } - } - if ( - features.isEnabled(EnablePGPPassphraseCache) && - BiometricAuthenticator.canAuthenticate(this@DecryptActivity) - ) { - BiometricAuthenticator.authenticate( - this@DecryptActivity, - R.string.biometric_prompt_title_gpg_passphrase_cache, - ) { authResult -> - requireKeysExist { decrypt(isError = false, authResult) } - } - } else { - requireKeysExist { decrypt(isError = false, BiometricResult.CanceledByUser) } - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.pgp_handler, menu) - passwordEntry?.let { entry -> - menu.findItem(R.id.edit_password).isVisible = true - if (!entry.password.isNullOrBlank()) { - menu.findItem(R.id.share_password_as_plaintext).isVisible = true - menu.findItem(R.id.copy_password).isVisible = true - } - } - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> onBackPressedDispatcher.onBackPressed() - R.id.edit_password -> editPassword() - R.id.share_password_as_plaintext -> shareAsPlaintext() - R.id.copy_password -> copyPasswordToClipboard(passwordEntry?.password) - else -> return super.onOptionsItemSelected(item) - } - return true - } - - /** - * Automatically finishes the activity after [PreferenceKeys.GENERAL_SHOW_TIME] seconds decryption - * succeeded to prevent information leaks from stale activities. - */ - private fun startAutoDismissTimer() { - lifecycleScope.launch { - val timeout = - settings.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() - ?: Constants.DEFAULT_DECRYPTION_TIMEOUT - if (timeout != 0) { - delay(timeout.seconds) - finish() - } - } - } - - /** - * 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.action = Intent.ACTION_VIEW - intent.putExtra("FILE_PATH", relativeParentPath) - intent.putExtra("REPO_PATH", repoPath) - intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name) - intent.putExtra(PasswordCreationActivity.EXTRA_USERNAME, passwordEntry?.username) - intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password) - intent.putExtra( - PasswordCreationActivity.EXTRA_EXTRA_CONTENT, - passwordEntry?.extraContentWithoutUsername, - ) - 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 decrypt(isError: Boolean, authResult: BiometricResult) { - val gpgIdentifiers = getPGPIdentifiers(relativeParentPath) ?: return - lifecycleScope.launch(dispatcherProvider.main()) { - when (authResult) { - // Internally handled by the prompt dialog - is BiometricResult.Retry -> {} - // If the dialog is dismissed for any reason, prompt for passphrase - is BiometricResult.CanceledByUser, - is BiometricResult.CanceledBySystem, - is BiometricResult.Failure, - is BiometricResult.HardwareUnavailableOrDisabled -> - askPassphrase(isError, gpgIdentifiers, authResult) - is BiometricResult.Success -> { - /* clear passphrase cache on first use after application startup or if screen was off; - also make sure to purge a stale cache after caching has been disabled via PGP settings */ - clearCache = settings.getBoolean(PreferenceKeys.CLEAR_PASSPHRASE_CACHE, true) - if (screenWasOff && clearCache) { - passphraseCache.clearAllCachedPassphrases(this@DecryptActivity) - screenWasOff = false - } - val cachedPassphrase = - passphraseCache.retrieveCachedPassphrase(this@DecryptActivity, gpgIdentifiers.first()) - if (cachedPassphrase != null) { - decryptWithPassphrase(cachedPassphrase, gpgIdentifiers, authResult) - } else { - askPassphrase(isError, gpgIdentifiers, authResult) - } - } - } - } - } - - private suspend fun askPassphrase( - isError: Boolean, - gpgIdentifiers: List<PGPIdentifier>, - authResult: BiometricResult, - ) { - if (retries < MAX_RETRIES) { - retries += 1 - } else { - finish() - } - if (!repository.isPasswordProtected(gpgIdentifiers)) { - decryptWithPassphrase(passphrase = "", gpgIdentifiers, authResult) - return - } - val dialog = - PasswordDialog.newInstance( - cacheEnabled = features.isEnabled(EnablePGPPassphraseCache), - clearCache = clearCache, - ) - if (isError) { - dialog.setError() - } - dialog.show(supportFragmentManager, "PASSWORD_DIALOG") - dialog.setFragmentResultListener(PasswordDialog.PASSWORD_RESULT_KEY) { key, bundle -> - if (key == PasswordDialog.PASSWORD_RESULT_KEY) { - val passphrase = bundle.getString(PasswordDialog.PASSWORD_PHRASE_KEY)!! - clearCache = bundle.getBoolean(PasswordDialog.PASSWORD_CLEAR_KEY) - lifecycleScope.launch(dispatcherProvider.main()) { - decryptWithPassphrase(passphrase, gpgIdentifiers, authResult) { - runCatching { - if (authResult is BiometricResult.Success) { - passphraseCache.cachePassphrase( - this@DecryptActivity, - gpgIdentifiers.first(), - passphrase, - ) - settings.edit { putBoolean(PreferenceKeys.CLEAR_PASSPHRASE_CACHE, clearCache) } - } - } - .onFailure { e -> logcat { e.asLog() } } - } - } - } - } - } - - private suspend fun decryptWithPassphrase( - passphrase: String, - identifiers: List<PGPIdentifier>, - authResult: BiometricResult, - onSuccess: suspend () -> Unit = {}, - ) { - val message = withContext(dispatcherProvider.io()) { File(fullPath).readBytes().inputStream() } - val outputStream = ByteArrayOutputStream() - val result = repository.decrypt(passphrase, identifiers, message, outputStream) - if (result.isOk) { - val entry = passwordEntryFactory.create(result.value.toByteArray()) - passwordEntry = entry - createPasswordUI(entry) - startAutoDismissTimer() - onSuccess() - } else { - logcat(ERROR) { result.error.stackTraceToString() } - decrypt(isError = true, authResult = authResult) - } - } - - private suspend fun createPasswordUI(entry: PasswordEntry) = - withContext(dispatcherProvider.main()) { - val labelFormat = resources.getString(R.string.otp_label_format) - val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true) - invalidateOptionsMenu() - - val items = arrayListOf<FieldItem>() - if (!entry.password.isNullOrBlank()) { - items.add(FieldItem.createPasswordField(getString(R.string.password), entry.password!!)) - if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) { - copyPasswordToClipboard(entry.password) - } - } - - if (entry.hasTotp()) { - items.add(FieldItem.createOtpField(labelFormat, entry.totp.first())) - } - - if (!entry.username.isNullOrBlank()) { - items.add(FieldItem.createUsernameField(getString(R.string.username), entry.username!!)) - } - - entry.extraContent.forEach { (key, value) -> - items.add(FieldItem.createFreeformField(key, value)) - } - - val adapter = FieldItemAdapter(items, showPassword) { text -> copyTextToClipboard(text) } - binding.recyclerView.adapter = adapter - binding.recyclerView.itemAnimator = null - - if (entry.hasTotp()) { - lifecycleScope.launch { entry.totp.collect { adapter.updateOTPCode(it, labelFormat) } } - } - } - - private companion object { - private const val MAX_RETRIES = 3 - } -} diff --git a/app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt b/app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt deleted file mode 100644 index f2a67706..00000000 --- a/app/src/main/java/app/passwordstore/ui/crypto/ViewPasswordScreen.kt +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.crypto - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.capitalize -import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.tooling.preview.Preview -import app.passwordstore.R -import app.passwordstore.data.passfile.PasswordEntry -import app.passwordstore.ui.APSAppBar -import app.passwordstore.ui.compose.CopyButton -import app.passwordstore.ui.compose.PasswordField -import app.passwordstore.ui.compose.theme.APSTheme -import app.passwordstore.ui.compose.theme.SpacingLarge -import app.passwordstore.ui.compose.theme.SpacingMedium -import app.passwordstore.util.time.UserClock -import app.passwordstore.util.totp.UriTotpFinder -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking - -/** Composable to show a decrypted [PasswordEntry]. */ -@Composable -fun ViewPasswordScreen( - entryName: String, - entry: PasswordEntry, - onNavigateUp: () -> Unit, - modifier: Modifier = Modifier, -) { - Scaffold( - topBar = { - APSAppBar( - title = entryName, - navigationIcon = painterResource(R.drawable.ic_arrow_back_black_24dp), - onNavigationIconClick = onNavigateUp, - backgroundColor = MaterialTheme.colorScheme.surface, - ) - } - ) { paddingValues -> - Box(modifier = modifier.padding(paddingValues)) { - Column( - modifier = - Modifier.padding(vertical = SpacingMedium, horizontal = SpacingLarge).fillMaxSize() - ) { - if (entry.password != null) { - PasswordField( - value = entry.password!!, - label = stringResource(R.string.password), - initialVisibility = false, - readOnly = true, - modifier = Modifier.padding(bottom = SpacingMedium).fillMaxWidth(), - ) - } - if (entry.hasTotp()) { - val totp by entry.totp.collectAsState(runBlocking { entry.totp.first() }) - TextField( - value = totp.value, - onValueChange = {}, - readOnly = true, - label = { Text("OTP (expires in ${totp.remainingTime.inWholeSeconds}s)") }, - trailingIcon = { CopyButton(totp.value, R.string.copy_label) }, - modifier = Modifier.padding(bottom = SpacingMedium).fillMaxWidth(), - ) - } - if (entry.username != null) { - TextField( - value = entry.username!!, - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(R.string.username)) }, - trailingIcon = { CopyButton(entry.username!!, R.string.copy_label) }, - modifier = Modifier.padding(bottom = SpacingMedium).fillMaxWidth(), - ) - } - ExtraContent(entry = entry) - } - } - } -} - -@Composable -private fun ExtraContent(entry: PasswordEntry, modifier: Modifier = Modifier) { - entry.extraContent.forEach { (label, value) -> - TextField( - value = value, - onValueChange = {}, - readOnly = true, - label = { Text(label.capitalize(Locale.current)) }, - trailingIcon = { CopyButton(value, R.string.copy_label) }, - modifier = modifier.padding(bottom = SpacingMedium).fillMaxWidth(), - ) - } -} - -@Preview -@Composable -private fun ViewPasswordScreenPreview() { - APSTheme { - ViewPasswordScreen(entryName = "Test Entry", entry = createTestEntry(), onNavigateUp = {}) - } -} - -private fun createTestEntry() = - PasswordEntry( - UserClock(), - UriTotpFinder(), - """ - |My Password - |otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 - |login: msfjarvis - |URL: example.com - """ - .trimMargin() - .encodeToByteArray(), - ) |
