mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-09-03 07:45:08 +00:00
Integrate PGPainless backend into the UI properly (#1647)
This commit is contained in:
@@ -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>
|
||||||
|
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
@@ -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,
|
||||||
)
|
)
|
||||||
|
@@ -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 = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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 = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
@@ -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("*/*"))
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 =
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
|
}
|
||||||
|
14
app/src/main/res/drawable/ic_password_24px.xml
Normal file
14
app/src/main/res/drawable/ic_password_24px.xml
Normal 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>
|
29
app/src/main/res/layout/dialog_password_entry.xml
Normal file
29
app/src/main/res/layout/dialog_password_entry.xml
Normal 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>
|
@@ -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>
|
||||||
|
Reference in New Issue
Block a user