Integrate PGPainless backend into the UI properly (#1647)

This commit is contained in:
Harsh Shandilya
2022-01-09 17:04:16 +05:30
committed by GitHub
parent 799f1393e4
commit 1738879fb3
16 changed files with 378 additions and 63 deletions

View File

@@ -158,6 +158,8 @@
android:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
android:theme="@style/DialogLikeThemeM3" android:theme="@style/DialogLikeThemeM3"
android:windowSoftInputMode="adjustNothing" /> android:windowSoftInputMode="adjustNothing" />
<activity android:name=".ui.pgp.PGPKeyImportActivity"
android:theme="@style/NoBackgroundThemeM3" />
</application> </application>
</manifest> </manifest>

View File

@@ -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,
)
}
}

View File

@@ -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

View File

@@ -21,10 +21,9 @@ import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.onSuccess import com.github.michaelbull.result.onSuccess
import com.github.michaelbull.result.runCatching import com.github.michaelbull.result.runCatching
import dagger.hilt.android.AndroidEntryPoint 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.data.passfile.PasswordEntry
import dev.msfjarvis.aps.injection.crypto.CryptoSet import dev.msfjarvis.aps.ui.crypto.PasswordDialog
import dev.msfjarvis.aps.ui.crypto.DecryptActivityV2
import dev.msfjarvis.aps.util.autofill.AutofillPreferences import dev.msfjarvis.aps.util.autofill.AutofillPreferences
import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder import dev.msfjarvis.aps.util.autofill.AutofillResponseBuilder
import dev.msfjarvis.aps.util.autofill.DirectoryStructure import dev.msfjarvis.aps.util.autofill.DirectoryStructure
@@ -33,6 +32,7 @@ import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import logcat.LogPriority.ERROR import logcat.LogPriority.ERROR
@@ -74,7 +74,7 @@ class AutofillDecryptActivityV2 : AppCompatActivity() {
} }
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
@Inject lateinit var cryptos: CryptoSet @Inject lateinit var repository: CryptoRepository
private lateinit var directoryStructure: DirectoryStructure private lateinit var directoryStructure: DirectoryStructure
@@ -98,43 +98,58 @@ class AutofillDecryptActivityV2 : AppCompatActivity() {
val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match
directoryStructure = AutofillPreferences.directoryStructure(this) directoryStructure = AutofillPreferences.directoryStructure(this)
logcat { action.toString() } logcat { action.toString() }
val dialog = PasswordDialog()
lifecycleScope.launch { lifecycleScope.launch {
val credentials = decryptCredential(File(filePath)) withContext(Dispatchers.Main) {
if (credentials == null) { dialog.password.collectLatest { value ->
setResult(RESULT_CANCELED) if (value != null) {
} else { decrypt(File(filePath), clientState, action, value)
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() }
} }
dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
} }
private suspend fun decryptCredential(file: File): Credentials? { private suspend fun decrypt(
runCatching { file.inputStream() } 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, password: String): Credentials? {
runCatching { file.readBytes().inputStream() }
.onFailure { e -> .onFailure { e ->
logcat(ERROR) { e.asLog("File to decrypt not found") } logcat(ERROR) { e.asLog("File to decrypt not found") }
return null return null
} }
.onSuccess { encryptedInput -> .onSuccess { encryptedInput ->
runCatching { runCatching {
val crypto = cryptos.first { it.canHandle(file.absolutePath) }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
crypto.decrypt( repository.decrypt(
Key(DecryptActivityV2.PRIV_KEY.encodeToByteArray()), password,
DecryptActivityV2.PASS,
encryptedInput, encryptedInput,
outputStream, outputStream,
) )

View File

@@ -12,11 +12,10 @@ import android.view.MenuItem
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.aps.R 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.passfile.PasswordEntry
import dev.msfjarvis.aps.data.password.FieldItem import dev.msfjarvis.aps.data.password.FieldItem
import dev.msfjarvis.aps.databinding.DecryptLayoutBinding import dev.msfjarvis.aps.databinding.DecryptLayoutBinding
import dev.msfjarvis.aps.injection.crypto.CryptoSet
import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter import dev.msfjarvis.aps.ui.adapters.FieldItemAdapter
import dev.msfjarvis.aps.util.extensions.unsafeLazy import dev.msfjarvis.aps.util.extensions.unsafeLazy
import dev.msfjarvis.aps.util.extensions.viewBinding import dev.msfjarvis.aps.util.extensions.viewBinding
@@ -29,6 +28,7 @@ import kotlin.time.ExperimentalTime
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -38,7 +38,7 @@ class DecryptActivityV2 : BasePgpActivity() {
private val binding by viewBinding(DecryptLayoutBinding::inflate) private val binding by viewBinding(DecryptLayoutBinding::inflate)
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory @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 val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) }
private var passwordEntry: PasswordEntry? = null private var passwordEntry: PasswordEntry? = null
@@ -127,16 +127,25 @@ class DecryptActivityV2 : BasePgpActivity() {
} }
private fun decrypt() { 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 { lifecycleScope.launch {
// TODO(msfjarvis): native methods are fallible, add error handling once out of testing val message = withContext(Dispatchers.IO) { File(fullPath).readBytes().inputStream() }
val message = withContext(Dispatchers.IO) { File(fullPath).inputStream() }
val result = val result =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val crypto = cryptos.first { it.canHandle(fullPath) }
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
crypto.decrypt( repository.decrypt(
Key(PRIV_KEY.encodeToByteArray()), password,
PASS,
message, message,
outputStream, 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 = ""
}
} }

View File

@@ -36,10 +36,9 @@ import com.google.zxing.integration.android.IntentIntegrator.QR_CODE
import com.google.zxing.qrcode.QRCodeReader import com.google.zxing.qrcode.QRCodeReader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.aps.R 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.passfile.PasswordEntry
import dev.msfjarvis.aps.databinding.PasswordCreationActivityBinding 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.DicewarePasswordGeneratorDialogFragment
import dev.msfjarvis.aps.ui.dialogs.OtpImportDialogFragment import dev.msfjarvis.aps.ui.dialogs.OtpImportDialogFragment
import dev.msfjarvis.aps.ui.dialogs.PasswordGeneratorDialogFragment import dev.msfjarvis.aps.ui.dialogs.PasswordGeneratorDialogFragment
@@ -70,7 +69,7 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
private val binding by viewBinding(PasswordCreationActivityBinding::inflate) private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory @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 suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) }
private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) } private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) }
@@ -364,15 +363,10 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
runCatching { runCatching {
val crypto = cryptos.first { it.canHandle(path) }
val result = val result =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
crypto.encrypt( repository.encrypt(content.byteInputStream(), outputStream)
listOf(Key(PUB_KEY.encodeToByteArray())),
content.byteInputStream(),
outputStream,
)
outputStream outputStream
} }
val file = File(path) val file = File(path)
@@ -484,7 +478,5 @@ class PasswordCreationActivityV2 : BasePgpActivity() {
const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT" const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT"
const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD" const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD"
const val EXTRA_EDITING = "EDITING" const val EXTRA_EDITING = "EDITING"
// TODO(msfjarvis): source this from storage
const val PUB_KEY = ""
} }
} }

View File

@@ -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()
}
}

View File

@@ -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("*/*"))
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -36,6 +36,7 @@ import dev.msfjarvis.aps.ui.sshkeygen.ShowSshKeyFragment
import dev.msfjarvis.aps.ui.sshkeygen.SshKeyGenActivity import dev.msfjarvis.aps.ui.sshkeygen.SshKeyGenActivity
import dev.msfjarvis.aps.ui.sshkeygen.SshKeyImportActivity import dev.msfjarvis.aps.ui.sshkeygen.SshKeyImportActivity
import dev.msfjarvis.aps.util.extensions.getString 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.sharedPrefs
import dev.msfjarvis.aps.util.extensions.snackbar import dev.msfjarvis.aps.util.extensions.snackbar
import dev.msfjarvis.aps.util.extensions.unsafeLazy import dev.msfjarvis.aps.util.extensions.unsafeLazy
@@ -59,16 +60,12 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
private var showSshKeyPref: Preference? = null private var showSshKeyPref: Preference? = null
private fun <T : FragmentActivity> launchActivity(clazz: Class<T>) {
activity.startActivity(Intent(activity, clazz))
}
private fun selectExternalGitRepository() { private fun selectExternalGitRepository() {
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
.setTitle(activity.resources.getString(R.string.external_repository_dialog_title)) .setTitle(activity.resources.getString(R.string.external_repository_dialog_title))
.setMessage(activity.resources.getString(R.string.external_repository_dialog_text)) .setMessage(activity.resources.getString(R.string.external_repository_dialog_text))
.setPositiveButton(R.string.dialog_ok) { _, _ -> .setPositiveButton(R.string.dialog_ok) { _, _ ->
launchActivity(DirectorySelectionActivity::class.java) activity.launchActivity(DirectorySelectionActivity::class.java)
} }
.setNegativeButton(R.string.dialog_cancel, null) .setNegativeButton(R.string.dialog_cancel, null)
.show() .show()
@@ -89,7 +86,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
titleRes = R.string.pref_edit_git_server_settings titleRes = R.string.pref_edit_git_server_settings
visible = PasswordRepository.isGitRepo() visible = PasswordRepository.isGitRepo()
onClick { onClick {
launchActivity(GitServerConfigActivity::class.java) activity.launchActivity(GitServerConfigActivity::class.java)
true true
} }
} }
@@ -97,7 +94,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
titleRes = R.string.pref_edit_proxy_settings titleRes = R.string.pref_edit_proxy_settings
visible = gitSettings.url?.startsWith("https") == true && PasswordRepository.isGitRepo() visible = gitSettings.url?.startsWith("https") == true && PasswordRepository.isGitRepo()
onClick { onClick {
launchActivity(ProxySelectorActivity::class.java) activity.launchActivity(ProxySelectorActivity::class.java)
true true
} }
} }
@@ -105,7 +102,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
titleRes = R.string.pref_edit_git_config titleRes = R.string.pref_edit_git_config
visible = PasswordRepository.isGitRepo() visible = PasswordRepository.isGitRepo()
onClick { onClick {
launchActivity(GitConfigActivity::class.java) activity.launchActivity(GitConfigActivity::class.java)
true true
} }
} }
@@ -113,7 +110,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
titleRes = R.string.pref_import_ssh_key_title titleRes = R.string.pref_import_ssh_key_title
visible = PasswordRepository.isGitRepo() visible = PasswordRepository.isGitRepo()
onClick { onClick {
launchActivity(SshKeyImportActivity::class.java) activity.launchActivity(SshKeyImportActivity::class.java)
true true
} }
} }

View File

@@ -24,6 +24,7 @@ class SettingsActivity : AppCompatActivity() {
private val passwordSettings = PasswordSettings(this) private val passwordSettings = PasswordSettings(this)
private val repositorySettings = RepositorySettings(this) private val repositorySettings = RepositorySettings(this)
private val generalSettings = GeneralSettings(this) private val generalSettings = GeneralSettings(this)
private val pgpSettings = PGPSettings(this)
private val binding by viewBinding(ActivityPreferenceRecyclerviewBinding::inflate) private val binding by viewBinding(ActivityPreferenceRecyclerviewBinding::inflate)
private val preferencesAdapter: PreferencesAdapter private val preferencesAdapter: PreferencesAdapter
@@ -47,7 +48,7 @@ class SettingsActivity : AppCompatActivity() {
} }
subScreen { subScreen {
titleRes = R.string.pref_category_passwords_title titleRes = R.string.pref_category_passwords_title
iconRes = R.drawable.ic_lock_open_24px iconRes = R.drawable.ic_password_24px
passwordSettings.provideSettings(this) passwordSettings.provideSettings(this)
} }
subScreen { subScreen {
@@ -60,6 +61,11 @@ class SettingsActivity : AppCompatActivity() {
iconRes = R.drawable.ic_miscellaneous_services_24px iconRes = R.drawable.ic_miscellaneous_services_24px
miscSettings.provideSettings(this) 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) val adapter = PreferencesAdapter(screen)
adapter.onScreenChangeListener = adapter.onScreenChangeListener =

View File

@@ -8,6 +8,7 @@ package dev.msfjarvis.aps.util.extensions
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
@@ -119,6 +120,11 @@ fun FragmentActivity.snackbar(
return 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 */ /** Simplifies the common `getString(key, null) ?: defaultValue` case slightly */
fun SharedPreferences.getString(key: String): String? = getString(key, null) fun SharedPreferences.getString(key: String): String? = getString(key, null)

View File

@@ -4,6 +4,9 @@
*/ */
package dev.msfjarvis.aps.util.extensions 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.getOrElse
import com.github.michaelbull.result.runCatching import com.github.michaelbull.result.runCatching
import dev.msfjarvis.aps.data.repo.PasswordRepository 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. */ /** A convenience extension to turn a [Throwable] with a message into a loggable string. */
fun Throwable.asLog(message: String): String = "$message\n${asLog()}" 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>
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -390,5 +390,9 @@
<string name="gpg_key_select_mandatory">Selecting a GPG key is necessary to proceed</string> <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="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="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> </resources>