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:theme="@style/DialogLikeThemeM3"
android:windowSoftInputMode="adjustNothing" />
<activity android:name=".ui.pgp.PGPKeyImportActivity"
android:theme="@style/NoBackgroundThemeM3" />
</application>
</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.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 decryptCredential(file: File): Credentials? {
runCatching { file.inputStream() }
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, 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,
)

View File

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

View File

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

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

View File

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

View File

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

View File

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

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="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>