diff options
Diffstat (limited to 'app')
16 files changed, 378 insertions, 63 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3ff6302..239cb4f8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -158,6 +158,8 @@ android:configChanges="orientation|keyboardHidden" android:theme="@style/DialogLikeThemeM3" android:windowSoftInputMode="adjustNothing" /> + <activity android:name=".ui.pgp.PGPKeyImportActivity" + android:theme="@style/NoBackgroundThemeM3" /> </application> </manifest> diff --git a/app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt b/app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt new file mode 100644 index 00000000..b0cde380 --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/data/crypto/CryptoRepository.kt @@ -0,0 +1,58 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.data.crypto + +import com.github.michaelbull.result.runCatching +import com.github.michaelbull.result.unwrap +import dev.msfjarvis.aps.crypto.PGPKeyManager +import dev.msfjarvis.aps.crypto.PGPainlessCryptoHandler +import dev.msfjarvis.aps.util.extensions.isOk +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class CryptoRepository +@Inject +constructor( + private val pgpKeyManager: PGPKeyManager, + private val pgpCryptoHandler: PGPainlessCryptoHandler, +) { + + suspend fun decrypt( + password: String, + message: ByteArrayInputStream, + out: ByteArrayOutputStream, + ) { + withContext(Dispatchers.IO) { decryptPgp(password, message, out) } + } + + suspend fun encrypt(content: ByteArrayInputStream, out: ByteArrayOutputStream) { + withContext(Dispatchers.IO) { encryptPgp(content, out) } + } + + private suspend fun decryptPgp( + password: String, + message: ByteArrayInputStream, + out: ByteArrayOutputStream, + ) { + val keys = pgpKeyManager.getAllKeys().unwrap() + // Iterates through the keys until the first successful decryption, then returns. + keys.first { key -> + runCatching { pgpCryptoHandler.decrypt(key, password, message, out) }.isOk() + } + } + + private suspend fun encryptPgp(content: ByteArrayInputStream, out: ByteArrayOutputStream) { + val keys = pgpKeyManager.getAllKeys().unwrap() + pgpCryptoHandler.encrypt( + keys, + content, + out, + ) + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/injection/crypto/KeyManagerModule.kt b/app/src/main/java/dev/msfjarvis/aps/injection/crypto/KeyManagerModule.kt new file mode 100644 index 00000000..0bc1c43d --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/injection/crypto/KeyManagerModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.injection.crypto + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dev.msfjarvis.aps.crypto.PGPKeyManager +import javax.inject.Qualifier +import kotlinx.coroutines.Dispatchers + +@Module +@InstallIn(SingletonComponent::class) +object KeyManagerModule { + @Provides + fun providePGPKeyManager( + @PGPKeyDir keyDir: String, + ): PGPKeyManager { + return PGPKeyManager( + keyDir, + Dispatchers.IO, + ) + } + + @Provides + @PGPKeyDir + fun providePGPKeyDir(@ApplicationContext context: Context): String { + return context.filesDir.resolve("pgp_keys").absolutePath + } +} + +@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class PGPKeyDir diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt index 8f4578cf..697d628d 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivityV2.kt @@ -21,10 +21,9 @@ import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onSuccess import com.github.michaelbull.result.runCatching import dagger.hilt.android.AndroidEntryPoint -import dev.msfjarvis.aps.crypto.Key +import dev.msfjarvis.aps.data.crypto.CryptoRepository import dev.msfjarvis.aps.data.passfile.PasswordEntry -import dev.msfjarvis.aps.injection.crypto.CryptoSet -import dev.msfjarvis.aps.ui.crypto.DecryptActivityV2 +import dev.msfjarvis.aps.ui.crypto.PasswordDialog import dev.msfjarvis.aps.util.autofill.AutofillPreferences import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder import dev.msfjarvis.aps.util.autofill.DirectoryStructure @@ -33,6 +32,7 @@ import java.io.ByteArrayOutputStream import java.io.File import javax.inject.Inject import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import logcat.LogPriority.ERROR @@ -74,7 +74,7 @@ class AutofillDecryptActivityV2 : AppCompatActivity() { } @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var cryptos: CryptoSet + @Inject lateinit var repository: CryptoRepository private lateinit var directoryStructure: DirectoryStructure @@ -98,43 +98,58 @@ class AutofillDecryptActivityV2 : AppCompatActivity() { val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match directoryStructure = AutofillPreferences.directoryStructure(this) logcat { action.toString() } + val dialog = PasswordDialog() lifecycleScope.launch { - val credentials = decryptCredential(File(filePath)) - if (credentials == null) { - setResult(RESULT_CANCELED) - } else { - val fillInDataset = - AutofillResponseBuilder.makeFillInDataset( - this@AutofillDecryptActivityV2, - credentials, - clientState, - action - ) - withContext(Dispatchers.Main) { - setResult( - RESULT_OK, - Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) } - ) + withContext(Dispatchers.Main) { + dialog.password.collectLatest { value -> + if (value != null) { + decrypt(File(filePath), clientState, action, value) + } } } - withContext(Dispatchers.Main) { finish() } } + dialog.show(supportFragmentManager, "PASSWORD_DIALOG") + } + + private suspend fun decrypt( + filePath: File, + clientState: Bundle, + action: AutofillAction, + password: String, + ) { + val credentials = decryptCredential(filePath, password) + if (credentials == null) { + setResult(RESULT_CANCELED) + } else { + val fillInDataset = + AutofillResponseBuilder.makeFillInDataset( + this@AutofillDecryptActivityV2, + credentials, + clientState, + action + ) + withContext(Dispatchers.Main) { + setResult( + RESULT_OK, + Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) } + ) + } + } + withContext(Dispatchers.Main) { finish() } } - private suspend fun decryptCredential(file: File): Credentials? { - runCatching { file.inputStream() } + private suspend fun decryptCredential(file: File, password: String): Credentials? { + runCatching { file.readBytes().inputStream() } .onFailure { e -> logcat(ERROR) { e.asLog("File to decrypt not found") } return null } .onSuccess { encryptedInput -> runCatching { - val crypto = cryptos.first { it.canHandle(file.absolutePath) } withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() - crypto.decrypt( - Key(DecryptActivityV2.PRIV_KEY.encodeToByteArray()), - DecryptActivityV2.PASS, + repository.decrypt( + password, encryptedInput, outputStream, ) diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt index 799ca32d..33ecdc2b 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivityV2.kt @@ -12,11 +12,10 @@ import android.view.MenuItem import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import dev.msfjarvis.aps.R -import dev.msfjarvis.aps.crypto.Key +import dev.msfjarvis.aps.data.crypto.CryptoRepository import dev.msfjarvis.aps.data.passfile.PasswordEntry import dev.msfjarvis.aps.data.password.FieldItem import dev.msfjarvis.aps.databinding.DecryptLayoutBinding -import dev.msfjarvis.aps.injection.crypto.CryptoSet import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter import dev.msfjarvis.aps.util.extensions.unsafeLazy import dev.msfjarvis.aps.util.extensions.viewBinding @@ -29,6 +28,7 @@ import kotlin.time.ExperimentalTime import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -38,7 +38,7 @@ class DecryptActivityV2 : BasePgpActivity() { private val binding by viewBinding(DecryptLayoutBinding::inflate) @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var cryptos: CryptoSet + @Inject lateinit var repository: CryptoRepository private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) } private var passwordEntry: PasswordEntry? = null @@ -127,16 +127,25 @@ class DecryptActivityV2 : BasePgpActivity() { } private fun decrypt() { + val dialog = PasswordDialog() + lifecycleScope.launch(Dispatchers.Main) { + dialog.password.collectLatest { value -> + if (value != null) { + decrypt(value) + } + } + } + dialog.show(supportFragmentManager, "PASSWORD_DIALOG") + } + + private fun decrypt(password: String) { lifecycleScope.launch { - // TODO(msfjarvis): native methods are fallible, add error handling once out of testing - val message = withContext(Dispatchers.IO) { File(fullPath).inputStream() } + val message = withContext(Dispatchers.IO) { File(fullPath).readBytes().inputStream() } val result = withContext(Dispatchers.IO) { - val crypto = cryptos.first { it.canHandle(fullPath) } val outputStream = ByteArrayOutputStream() - crypto.decrypt( - Key(PRIV_KEY.encodeToByteArray()), - PASS, + repository.decrypt( + password, message, outputStream, ) @@ -179,10 +188,4 @@ class DecryptActivityV2 : BasePgpActivity() { } } } - - companion object { - // TODO(msfjarvis): source these from storage and user input - const val PRIV_KEY = "" - const val PASS = "" - } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt index ab5b6371..a191ba61 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivityV2.kt @@ -36,10 +36,9 @@ import com.google.zxing.integration.android.IntentIntegrator.QR_CODE import com.google.zxing.qrcode.QRCodeReader import dagger.hilt.android.AndroidEntryPoint import dev.msfjarvis.aps.R -import dev.msfjarvis.aps.crypto.Key +import dev.msfjarvis.aps.data.crypto.CryptoRepository import dev.msfjarvis.aps.data.passfile.PasswordEntry import dev.msfjarvis.aps.databinding.PasswordCreationActivityBinding -import dev.msfjarvis.aps.injection.crypto.CryptoSet import dev.msfjarvis.aps.ui.dialogs.DicewarePasswordGeneratorDialogFragment import dev.msfjarvis.aps.ui.dialogs.OtpImportDialogFragment import dev.msfjarvis.aps.ui.dialogs.PasswordGeneratorDialogFragment @@ -70,7 +69,7 @@ class PasswordCreationActivityV2 : BasePgpActivity() { private val binding by viewBinding(PasswordCreationActivityBinding::inflate) @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var cryptos: CryptoSet + @Inject lateinit var repository: CryptoRepository private val suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) } private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) } @@ -364,15 +363,10 @@ class PasswordCreationActivityV2 : BasePgpActivity() { lifecycleScope.launch(Dispatchers.Main) { runCatching { - val crypto = cryptos.first { it.canHandle(path) } val result = withContext(Dispatchers.IO) { val outputStream = ByteArrayOutputStream() - crypto.encrypt( - listOf(Key(PUB_KEY.encodeToByteArray())), - content.byteInputStream(), - outputStream, - ) + repository.encrypt(content.byteInputStream(), outputStream) outputStream } val file = File(path) @@ -484,7 +478,5 @@ class PasswordCreationActivityV2 : BasePgpActivity() { const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT" const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD" const val EXTRA_EDITING = "EDITING" - // TODO(msfjarvis): source this from storage - const val PUB_KEY = "" } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt new file mode 100644 index 00000000..3542422a --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordDialog.kt @@ -0,0 +1,42 @@ +/* + * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.crypto + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.databinding.DialogPasswordEntryBinding +import dev.msfjarvis.aps.util.extensions.finish +import dev.msfjarvis.aps.util.extensions.unsafeLazy +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** [DialogFragment] to request a password from the user and forward it along. */ +class PasswordDialog : DialogFragment() { + + private val binding by unsafeLazy { DialogPasswordEntryBinding.inflate(layoutInflater) } + private val _password = MutableStateFlow<String?>(null) + val password = _password.asStateFlow() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setView(binding.root) + builder.setTitle(R.string.password) + builder.setPositiveButton(android.R.string.ok) { _, _ -> + do {} while (!_password.tryEmit(binding.passwordEditText.text.toString())) + dismiss() + } + return builder.create() + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + finish() + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/pgp/PGPKeyImportActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/pgp/PGPKeyImportActivity.kt new file mode 100644 index 00000000..3cd6930c --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/pgp/PGPKeyImportActivity.kt @@ -0,0 +1,67 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +@file:Suppress("BlockingMethodInNonBlockingContext") + +package dev.msfjarvis.aps.ui.pgp + +import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts.OpenDocument +import androidx.appcompat.app.AppCompatActivity +import com.github.michaelbull.result.mapBoth +import com.github.michaelbull.result.runCatching +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.crypto.Key +import dev.msfjarvis.aps.crypto.KeyUtils.tryGetId +import dev.msfjarvis.aps.crypto.PGPKeyManager +import javax.inject.Inject +import kotlinx.coroutines.runBlocking + +@AndroidEntryPoint +class PGPKeyImportActivity : AppCompatActivity() { + + @Inject lateinit var keyManager: PGPKeyManager + + private val pgpKeyImportAction = + registerForActivityResult(OpenDocument()) { uri -> + runCatching { + if (uri == null) { + throw IllegalStateException("Selected URI was null") + } + val keyInputStream = + contentResolver.openInputStream(uri) + ?: throw IllegalStateException("Failed to open selected file") + val bytes = keyInputStream.readBytes() + val (key, error) = runBlocking { keyManager.addKey(Key(bytes)) } + if (error != null) throw error + key + } + .mapBoth( + { key -> + require(key != null) { "Key cannot be null here" } + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.pgp_key_import_succeeded)) + .setMessage(getString(R.string.pgp_key_import_succeeded_message, tryGetId(key))) + .setPositiveButton(android.R.string.ok) { _, _ -> finish() } + .setOnCancelListener { finish() } + .show() + }, + { throwable -> + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.pgp_key_import_failed)) + .setMessage(throwable.message) + .setPositiveButton(android.R.string.ok) { _, _ -> finish() } + .setOnCancelListener { finish() } + .show() + } + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + pgpKeyImportAction.launch(arrayOf("*/*")) + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/PGPSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/PGPSettings.kt new file mode 100644 index 00000000..81418ccb --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/PGPSettings.kt @@ -0,0 +1,29 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.settings + +import androidx.fragment.app.FragmentActivity +import de.Maxr1998.modernpreferences.PreferenceScreen +import de.Maxr1998.modernpreferences.helpers.onClick +import de.Maxr1998.modernpreferences.helpers.pref +import dev.msfjarvis.aps.ui.pgp.PGPKeyImportActivity +import dev.msfjarvis.aps.util.extensions.launchActivity + +class PGPSettings(private val activity: FragmentActivity) : SettingsProvider { + + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + pref("_") { + title = "Import PGP key" + persistent = false + onClick { + activity.launchActivity(PGPKeyImportActivity::class.java) + false + } + } + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt index 8671b5cb..cbd9c18f 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt @@ -36,6 +36,7 @@ import dev.msfjarvis.aps.ui.sshkeygen.ShowSshKeyFragment import dev.msfjarvis.aps.ui.sshkeygen.SshKeyGenActivity import dev.msfjarvis.aps.ui.sshkeygen.SshKeyImportActivity import dev.msfjarvis.aps.util.extensions.getString +import dev.msfjarvis.aps.util.extensions.launchActivity import dev.msfjarvis.aps.util.extensions.sharedPrefs import dev.msfjarvis.aps.util.extensions.snackbar import dev.msfjarvis.aps.util.extensions.unsafeLazy @@ -59,16 +60,12 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi private var showSshKeyPref: Preference? = null - private fun <T : FragmentActivity> launchActivity(clazz: Class<T>) { - activity.startActivity(Intent(activity, clazz)) - } - private fun selectExternalGitRepository() { MaterialAlertDialogBuilder(activity) .setTitle(activity.resources.getString(R.string.external_repository_dialog_title)) .setMessage(activity.resources.getString(R.string.external_repository_dialog_text)) .setPositiveButton(R.string.dialog_ok) { _, _ -> - launchActivity(DirectorySelectionActivity::class.java) + activity.launchActivity(DirectorySelectionActivity::class.java) } .setNegativeButton(R.string.dialog_cancel, null) .show() @@ -89,7 +86,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi titleRes = R.string.pref_edit_git_server_settings visible = PasswordRepository.isGitRepo() onClick { - launchActivity(GitServerConfigActivity::class.java) + activity.launchActivity(GitServerConfigActivity::class.java) true } } @@ -97,7 +94,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi titleRes = R.string.pref_edit_proxy_settings visible = gitSettings.url?.startsWith("https") == true && PasswordRepository.isGitRepo() onClick { - launchActivity(ProxySelectorActivity::class.java) + activity.launchActivity(ProxySelectorActivity::class.java) true } } @@ -105,7 +102,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi titleRes = R.string.pref_edit_git_config visible = PasswordRepository.isGitRepo() onClick { - launchActivity(GitConfigActivity::class.java) + activity.launchActivity(GitConfigActivity::class.java) true } } @@ -113,7 +110,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi titleRes = R.string.pref_import_ssh_key_title visible = PasswordRepository.isGitRepo() onClick { - launchActivity(SshKeyImportActivity::class.java) + activity.launchActivity(SshKeyImportActivity::class.java) true } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt index 26f060ad..d31aa630 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt @@ -24,6 +24,7 @@ class SettingsActivity : AppCompatActivity() { private val passwordSettings = PasswordSettings(this) private val repositorySettings = RepositorySettings(this) private val generalSettings = GeneralSettings(this) + private val pgpSettings = PGPSettings(this) private val binding by viewBinding(ActivityPreferenceRecyclerviewBinding::inflate) private val preferencesAdapter: PreferencesAdapter @@ -47,7 +48,7 @@ class SettingsActivity : AppCompatActivity() { } subScreen { titleRes = R.string.pref_category_passwords_title - iconRes = R.drawable.ic_lock_open_24px + iconRes = R.drawable.ic_password_24px passwordSettings.provideSettings(this) } subScreen { @@ -60,6 +61,11 @@ class SettingsActivity : AppCompatActivity() { iconRes = R.drawable.ic_miscellaneous_services_24px miscSettings.provideSettings(this) } + subScreen { + titleRes = R.string.pref_category_pgp_title + iconRes = R.drawable.ic_lock_open_24px + pgpSettings.provideSettings(this) + } } val adapter = PreferencesAdapter(screen) adapter.onScreenChangeListener = diff --git a/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt b/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt index 7671adb4..4e045715 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt +++ b/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt @@ -8,6 +8,7 @@ package dev.msfjarvis.aps.util.extensions import android.app.KeyguardManager import android.content.ClipboardManager import android.content.Context +import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager import android.os.Build @@ -119,6 +120,11 @@ fun FragmentActivity.snackbar( return snackbar } +/** Launch an activity denoted by [clazz]. */ +fun <T : FragmentActivity> FragmentActivity.launchActivity(clazz: Class<T>) { + startActivity(Intent(this, clazz)) +} + /** Simplifies the common `getString(key, null) ?: defaultValue` case slightly */ fun SharedPreferences.getString(key: String): String? = getString(key, null) diff --git a/app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt b/app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt index 6c779af6..6e14a29d 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt +++ b/app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt @@ -4,6 +4,9 @@ */ package dev.msfjarvis.aps.util.extensions +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.runCatching import dev.msfjarvis.aps.data.repo.PasswordRepository @@ -82,3 +85,13 @@ fun <T> unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE) { ini /** A convenience extension to turn a [Throwable] with a message into a loggable string. */ fun Throwable.asLog(message: String): String = "$message\n${asLog()}" + +/** Extension on [Result] that returns if the type is [Ok] */ +fun <V, E> Result<V, E>.isOk(): Boolean { + return this is Ok<V> +} + +/** Extension on [Result] that returns if the type is [Err] */ +fun <V, E> Result<V, E>.isErr(): Boolean { + return this is Err<E> +} diff --git a/app/src/main/res/drawable/ic_password_24px.xml b/app/src/main/res/drawable/ic_password_24px.xml new file mode 100644 index 00000000..a41a383a --- /dev/null +++ b/app/src/main/res/drawable/ic_password_24px.xml @@ -0,0 +1,14 @@ +<!-- + ~ Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + ~ SPDX-License-Identifier: GPL-3.0-only + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M2,17h20v2H2V17zM3.15,12.95L4,11.47l0.85,1.48l1.3,-0.75L5.3,10.72H7v-1.5H5.3l0.85,-1.47L4.85,7L4,8.47L3.15,7l-1.3,0.75L2.7,9.22H1v1.5h1.7L1.85,12.2L3.15,12.95zM9.85,12.2l1.3,0.75L12,11.47l0.85,1.48l1.3,-0.75l-0.85,-1.48H15v-1.5h-1.7l0.85,-1.47L12.85,7L12,8.47L11.15,7l-1.3,0.75l0.85,1.47H9v1.5h1.7L9.85,12.2zM23,9.22h-1.7l0.85,-1.47L20.85,7L20,8.47L19.15,7l-1.3,0.75l0.85,1.47H17v1.5h1.7l-0.85,1.48l1.3,0.75L20,11.47l0.85,1.48l1.3,-0.75l-0.85,-1.48H23V9.22z" + android:fillColor="#000000"/> +</vector> diff --git a/app/src/main/res/layout/dialog_password_entry.xml b/app/src/main/res/layout/dialog_password_entry.xml new file mode 100644 index 00000000..3729fb4f --- /dev/null +++ b/app/src/main/res/layout/dialog_password_entry.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. + ~ SPDX-License-Identifier: GPL-3.0-only + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="@dimen/activity_horizontal_margin"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/password_field" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/password" + app:endIconMode="password_toggle" + app:hintAnimationEnabled="true" + app:hintEnabled="true"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/password_edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textPassword" /> + </com.google.android.material.textfield.TextInputLayout> + +</LinearLayout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2eda6fa..432ee6ea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -390,5 +390,9 @@ <string name="gpg_key_select_mandatory">Selecting a GPG key is necessary to proceed</string> <string name="place_shortcut_on_home_screen">Place shortcut on home screen</string> <string name="password_list_fab_content_description">Create new password or folder</string> + <string name="pgp_key_import_failed">Failed to import PGP key</string> + <string name="pgp_key_import_succeeded">Successfully imported PGP key</string> + <string name="pgp_key_import_succeeded_message">The key ID of the imported key is given below, please review it for correctness:\n%1$s</string> + <string name="pref_category_pgp_title">PGP settings</string> </resources> |