Change main credential validation

This commit is contained in:
J-Jamet
2022-05-10 19:59:56 +02:00
parent 8b2f994769
commit 327c9de464
9 changed files with 169 additions and 96 deletions

View File

@@ -36,6 +36,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
@@ -53,7 +54,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private lateinit var rootView: View private lateinit var rootView: View
private lateinit var passwordCheckBox: CompoundButton private lateinit var passwordCheckBox: CompoundButton
private lateinit var passKeyView: PassKeyView private lateinit var passwordView: PassKeyView
private lateinit var passwordRepeatTextInputLayout: TextInputLayout private lateinit var passwordRepeatTextInputLayout: TextInputLayout
private lateinit var passwordRepeatView: TextView private lateinit var passwordRepeatView: TextView
@@ -139,7 +140,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
} }
passwordCheckBox = rootView.findViewById(R.id.password_checkbox) passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
passKeyView = rootView.findViewById(R.id.password_view) passwordView = rootView.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout) passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView.findViewById(R.id.password_confirmation) passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
passwordRepeatView.applyFontVisibility() passwordRepeatView.applyFontVisibility()
@@ -165,8 +166,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
} }
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper) keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
hardwareKeySelectionView.selectionListener = { _ -> hardwareKeySelectionView.selectionListener = { hardwareKey ->
hardwareKeyCheckBox.isChecked = true hardwareKeyCheckBox.isChecked = true
hardwareKeySelectionView.error =
if (!HardwareKeyResponseHelper.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
// show hardware driver dialog if required
getString(R.string.warning_hardware_key_required)
} else {
null
}
} }
val dialog = builder.create() val dialog = builder.create()
@@ -194,18 +202,41 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
} }
private fun approveMainCredential() { private fun approveMainCredential() {
var error = verifyPassword() || verifyKeyFile() || verifyHardwareKey() val errorPassword = verifyPassword()
if (!passwordCheckBox.isChecked val errorKeyFile = verifyKeyFile()
&& !keyFileCheckBox.isChecked val errorHardwareKey = verifyHardwareKey()
&& !hardwareKeyCheckBox.isChecked // Check all to fill error
var error = errorPassword || errorKeyFile || errorHardwareKey
val hardwareKey = hardwareKeySelectionView.hardwareKey
if (!error
&& (!passwordCheckBox.isChecked)
&& (!keyFileCheckBox.isChecked)
&& (!hardwareKeyCheckBox.isChecked)
) { ) {
error = true error = true
if (mAllowNoMasterKey) if (mAllowNoMasterKey) {
// show no key dialog if required
showNoKeyConfirmationDialog() showNoKeyConfirmationDialog()
else { } else {
passwordRepeatTextInputLayout.error = passwordRepeatTextInputLayout.error =
getString(R.string.error_disallow_no_credentials) getString(R.string.error_disallow_no_credentials)
} }
} else if (!error
&& mMasterPassword.isNullOrEmpty()
&& !keyFileCheckBox.isChecked
&& !hardwareKeyCheckBox.isChecked
) {
// show empty password dialog if required
error = true
showEmptyPasswordConfirmationDialog()
} else if (!error
&& hardwareKey != null
&& !HardwareKeyResponseHelper.isHardwareKeyAvailable(
requireActivity(), hardwareKey, false)
) {
// show hardware driver dialog if required
error = true
hardwareKeySelectionView.error = getString(R.string.warning_hardware_key_required)
} }
if (!error) { if (!error) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
@@ -213,30 +244,11 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
} }
} }
private fun retrieveMainCredential(): MainCredential {
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
return MainCredential(masterPassword, keyFileUri, hardwareKey)
}
override fun onResume() {
super.onResume()
// To check checkboxes if a text is present
passKeyView.addTextChangedListener(passwordTextWatcher)
}
override fun onPause() {
super.onPause()
passKeyView.removeTextChangedListener(passwordTextWatcher)
}
private fun verifyPassword(): Boolean { private fun verifyPassword(): Boolean {
var error = false var error = false
passwordRepeatTextInputLayout.error = null
if (passwordCheckBox.isChecked) { if (passwordCheckBox.isChecked) {
mMasterPassword = passKeyView.passwordString mMasterPassword = passwordView.passwordString
val confPassword = passwordRepeatView.text.toString() val confPassword = passwordRepeatView.text.toString()
// Verify that passwords match // Verify that passwords match
@@ -245,21 +257,13 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
// Passwords do not match // Passwords do not match
passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match) passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match)
} }
if ((mMasterPassword.isNullOrEmpty())
&& (!keyFileCheckBox.isChecked
|| keyFileSelectionView.uri == null)
&& (!hardwareKeyCheckBox.isChecked
|| hardwareKeySelectionView.hardwareKey == null)) {
error = true
showEmptyPasswordConfirmationDialog()
}
} }
return error return error
} }
private fun verifyKeyFile(): Boolean { private fun verifyKeyFile(): Boolean {
var error = false var error = false
keyFileSelectionView.error = null
if (keyFileCheckBox.isChecked) { if (keyFileCheckBox.isChecked) {
keyFileSelectionView.uri?.let { uri -> keyFileSelectionView.uri?.let { uri ->
mKeyFileUri = uri mKeyFileUri = uri
@@ -273,26 +277,46 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private fun verifyHardwareKey(): Boolean { private fun verifyHardwareKey(): Boolean {
var error = false var error = false
hardwareKeySelectionView.error = null
if (hardwareKeyCheckBox.isChecked) { if (hardwareKeyCheckBox.isChecked) {
hardwareKeySelectionView.hardwareKey.let { hardwareKey -> hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
mHardwareKey = hardwareKey mHardwareKey = hardwareKey
} ?: run {
error = true
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
} }
// TODO verify drivers
// error = true
} }
return error return error
} }
private fun retrieveMainCredential(): MainCredential {
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
return MainCredential(masterPassword, keyFileUri, hardwareKey)
}
override fun onResume() {
super.onResume()
// To check checkboxes if a text is present
passwordView.addTextChangedListener(passwordTextWatcher)
}
override fun onPause() {
super.onPause()
passwordView.removeTextChangedListener(passwordTextWatcher)
}
private fun showEmptyPasswordConfirmationDialog() { private fun showEmptyPasswordConfirmationDialog() {
activity?.let { activity?.let {
val builder = AlertDialog.Builder(it) val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password) builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@SetMainCredentialDialogFragment.dismiss() this@SetMainCredentialDialogFragment.dismiss()
} }
}
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
mEmptyPasswordConfirmationDialog = builder.create() mEmptyPasswordConfirmationDialog = builder.create()
mEmptyPasswordConfirmationDialog?.show() mEmptyPasswordConfirmationDialog?.show()

View File

@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
@@ -84,7 +85,7 @@ import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import kotlinx.coroutines.* import kotlinx.coroutines.launch
import java.util.* import java.util.*
/** /**
@@ -134,8 +135,12 @@ class DatabaseTaskProvider {
respondToChallengeIfAllowed() respondToChallengeIfAllowed()
} }
this.requestChallengeListener = object: DatabaseTaskNotificationService.RequestChallengeListener { this.requestChallengeListener = object: DatabaseTaskNotificationService.RequestChallengeListener {
override fun onChallengeResponseRequested(hardwareKey: HardwareKey?, seed: ByteArray?) { override fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?) {
if (HardwareKeyResponseHelper.isHardwareKeyAvailable(activity, hardwareKey)) {
mHardwareKeyResponseHelper?.launchChallengeForResponse(hardwareKey, seed) mHardwareKeyResponseHelper?.launchChallengeForResponse(hardwareKey, seed)
} else {
throw InvalidCredentialsDatabaseException("Driver for $hardwareKey is required.")
}
} }
} }
} else { } else {

View File

@@ -4,13 +4,17 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.launch
class HardwareKeyResponseHelper { class HardwareKeyResponseHelper {
@@ -32,14 +36,13 @@ class HardwareKeyResponseHelper {
fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?, fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?,
extra: Bundle?) -> Unit) { extra: Bundle?) -> Unit) {
val resultCallback = ActivityResultCallback<ActivityResult> { result -> val resultCallback = ActivityResultCallback<ActivityResult> { result ->
Log.d(TAG, "resultCode from ykdroid: " + result.resultCode)
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra("response") val challengeResponse: ByteArray? = result.data?.getByteArrayExtra("response")
Log.d(TAG, "Response: " + challengeResponse.contentToString()) Log.d(TAG, "Response form challenge : " + challengeResponse.contentToString())
onChallengeResponded.invoke(challengeResponse, onChallengeResponded.invoke(challengeResponse,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY)) result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
} else { } else {
Log.e(TAG, "Response error") Log.e(TAG, "Response from challenge error")
onChallengeResponded.invoke(null, onChallengeResponded.invoke(null,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY)) result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
} }
@@ -58,9 +61,12 @@ class HardwareKeyResponseHelper {
} }
} }
fun launchChallengeForResponse(hardwareKey: HardwareKey?, seed: ByteArray?) { fun launchChallengeForResponse(hardwareKey: HardwareKey, seed: ByteArray?) {
try {
when (hardwareKey) { when (hardwareKey) {
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2
throw Exception("FIDO2 not implemented")
}
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Transform the seed before sending // Transform the seed before sending
var challenge: ByteArray? = null var challenge: ByteArray? = null
@@ -70,37 +76,50 @@ class HardwareKeyResponseHelper {
challenge.fill(32, 32, 64) challenge.fill(32, 32, 64)
} }
// Send to the driver // Send to the driver
getChallengeResponseResultLauncher?.launch(Intent(YKDROID_CHALLENGE_RESPONSE_INTENT).apply { getChallengeResponseResultLauncher!!.launch(Intent(YKDROID_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(YKDROID_SEED_KEY, challenge) putExtra(YKDROID_SEED_KEY, challenge)
}) })
Log.d(TAG, "Challenge sent : " + challenge.contentToString()) Log.d(TAG, "Challenge sent : " + challenge.contentToString())
} }
else -> {
// TODO other algorithm
}
}
} catch (e: Exception) {
Log.e(
TAG,
"Unable to retrieve the challenge response",
e
)
e.message?.let { message ->
Toast.makeText(
activity,
message,
Toast.LENGTH_LONG
).show()
}
} }
} }
companion object { companion object {
private val TAG = HardwareKeyResponseHelper::class.java.simpleName private val TAG = HardwareKeyResponseHelper::class.java.simpleName
private const val YKDROID_CHALLENGE_RESPONSE_INTENT = "net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE" private const val YKDROID_PACKAGE = "net.pp3345.ykdroid"
private const val YKDROID_CHALLENGE_RESPONSE_INTENT =
"$YKDROID_PACKAGE.intent.action.CHALLENGE_RESPONSE"
private const val YKDROID_SEED_KEY = "challenge" private const val YKDROID_SEED_KEY = "challenge"
private const val EXTRA_BUNDLE_KEY = "EXTRA_BUNDLE_KEY" private const val EXTRA_BUNDLE_KEY = "EXTRA_BUNDLE_KEY"
fun isHardwareKeyAvailable(activity: FragmentActivity,
hardwareKey: HardwareKey,
showDialog: Boolean = true): Boolean {
return when (hardwareKey) {
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2
if (showDialog)
showHardwareKeyDriverNeeded(activity)
false
}
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// TODO (UriUtil.isExternalAppInstalled(activity, KEEPASSDX_PRO_PACKAGE)
UriUtil.isExternalAppInstalled(activity, YKDROID_PACKAGE)
}
}
}
private fun showHardwareKeyDriverNeeded(activity: FragmentActivity) {
activity.lifecycleScope.launch {
val builder = AlertDialog.Builder(activity)
builder.setMessage(R.string.warning_hardware_key_required)
.setPositiveButton(android.R.string.ok) { _, _ ->
UriUtil.openExternalApp(activity, UriUtil.KEEPASSDX_PRO_PACKAGE)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
} }
} }

View File

@@ -70,7 +70,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private var mActionTaskBinder = ActionTaskBinder() private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = mutableListOf<ActionTaskListener>() private var mActionTaskListeners = mutableListOf<ActionTaskListener>()
// Channel to connect asynchronously a listener or a response // Channel to connect asynchronously a listener or a response
private var mRequestChallengeListenerChannel = Channel<RequestChallengeListener?>(0) private var mRequestChallengeListenerChannel = Channel<RequestChallengeListener>(0)
private var mResponseChallengeChannel = Channel<ByteArray?>(0) private var mResponseChallengeChannel = Channel<ByteArray?>(0)
private var mActionRunning = false private var mActionRunning = false
@@ -156,7 +156,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
interface RequestChallengeListener { interface RequestChallengeListener {
fun onChallengeResponseRequested(hardwareKey: HardwareKey?, seed: ByteArray?) fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?)
} }
fun checkDatabase() { fun checkDatabase() {
@@ -598,7 +598,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
runBlocking { runBlocking {
// Send the request // Send the request
val challengeResponseRequestListener = mRequestChallengeListenerChannel.receive() val challengeResponseRequestListener = mRequestChallengeListenerChannel.receive()
challengeResponseRequestListener?.onChallengeResponseRequested(hardwareKey, seed) challengeResponseRequestListener.onChallengeResponseRequested(hardwareKey, seed)
// Wait the response // Wait the response
response = mResponseChallengeChannel.receive() ?: byteArrayOf() response = mResponseChallengeChannel.receive() ?: byteArrayOf()
} }

View File

@@ -269,11 +269,11 @@ object UriUtil {
fun contributingUser(context: Context): Boolean { fun contributingUser(context: Context): Boolean {
return (Education.isEducationScreenReclickedPerformed(context) return (Education.isEducationScreenReclickedPerformed(context)
|| isExternalAppInstalled(context, "com.kunzisoft.keepass.pro", false) || isExternalAppInstalled(context, KEEPASSDX_PRO_PACKAGE, false)
) )
} }
private fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean { fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean {
try { try {
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES) context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
Education.setEducationScreenReclickedPerformed(context) Education.setEducationScreenReclickedPerformed(context)
@@ -317,4 +317,6 @@ object UriUtil {
} }
private const val TAG = "UriUtil" private const val TAG = "UriUtil"
const val KEEPASSDX_PRO_PACKAGE = "com.kunzisoft.keepass.pro"
} }

View File

@@ -12,6 +12,7 @@ import android.widget.ArrayAdapter
import android.widget.Filter import android.widget.Filter
import androidx.appcompat.widget.AppCompatAutoCompleteTextView import androidx.appcompat.widget.AppCompatAutoCompleteTextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.readEnum import com.kunzisoft.keepass.utils.readEnum
@@ -25,8 +26,9 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context,
private var mHardwareKey: HardwareKey? = null private var mHardwareKey: HardwareKey? = null
private val hardwareKeyLayout: TextInputLayout
private val hardwareKeyCompletion: AppCompatAutoCompleteTextView private val hardwareKeyCompletion: AppCompatAutoCompleteTextView
var selectionListener: ((HardwareKey?)-> Unit)? = null var selectionListener: ((HardwareKey)-> Unit)? = null
private val mHardwareKeyAdapter = ArrayAdapterNoFilter(context) private val mHardwareKeyAdapter = ArrayAdapterNoFilter(context)
@@ -66,6 +68,7 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context,
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_hardware_key_selection, this) inflater?.inflate(R.layout.view_hardware_key_selection, this)
hardwareKeyLayout = findViewById(R.id.input_entry_hardware_key_layout)
hardwareKeyCompletion = findViewById(R.id.input_entry_hardware_key_completion) hardwareKeyCompletion = findViewById(R.id.input_entry_hardware_key_completion)
hardwareKeyCompletion.inputType = InputType.TYPE_NULL hardwareKeyCompletion.inputType = InputType.TYPE_NULL
@@ -74,7 +77,9 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context,
hardwareKeyCompletion.onItemClickListener = hardwareKeyCompletion.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ -> AdapterView.OnItemClickListener { _, _, position, _ ->
mHardwareKey = HardwareKey.fromPosition(position) mHardwareKey = HardwareKey.fromPosition(position)
selectionListener?.invoke(mHardwareKey) mHardwareKey?.let { hardwareKey ->
selectionListener?.invoke(hardwareKey)
}
} }
} }
@@ -88,6 +93,12 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context,
hardwareKeyCompletion.setSelection(value.ordinal) hardwareKeyCompletion.setSelection(value.ordinal)
} }
var error: CharSequence?
get() = hardwareKeyLayout.error
set(value) {
hardwareKeyLayout.error = value
}
override fun onSaveInstanceState(): Parcelable { override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState() val superState = super.onSaveInstanceState()
val saveState = SavedState(superState) val saveState = SavedState(superState)

View File

@@ -107,9 +107,19 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
onPasswordChecked?.onCheckedChanged(view, checked) onPasswordChecked?.onCheckedChanged(view, checked)
} }
checkboxKeyFileView.setOnCheckedChangeListener { view, checked -> checkboxKeyFileView.setOnCheckedChangeListener { view, checked ->
if (checked) {
if (keyFileSelectionView.uri == null) {
checkboxKeyFileView.isChecked = false
}
}
onKeyFileChecked?.onCheckedChanged(view, checked) onKeyFileChecked?.onCheckedChanged(view, checked)
} }
checkboxHardwareView.setOnCheckedChangeListener { view, checked -> checkboxHardwareView.setOnCheckedChangeListener { view, checked ->
if (checked) {
if (hardwareKeySelectionView.hardwareKey == null) {
checkboxHardwareView.isChecked = false
}
}
onHardwareKeyChecked?.onCheckedChanged(view, checked) onHardwareKeyChecked?.onCheckedChanged(view, checked)
} }
@@ -152,8 +162,8 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
fun isFill(): Boolean { fun isFill(): Boolean {
return checkboxPasswordView.isChecked return checkboxPasswordView.isChecked
|| checkboxKeyFileView.isChecked // TODO better recognition || (checkboxKeyFileView.isChecked && keyFileSelectionView.uri != null)
|| checkboxHardwareView.isChecked || (checkboxHardwareView.isChecked && hardwareKeySelectionView.hardwareKey != null)
} }
fun getMainCredential(): MainCredential { fun getMainCredential(): MainCredential {

View File

@@ -12,7 +12,7 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
style="@style/KeepassDXStyle.TextInputLayout.ExposedMenu" style="@style/KeepassDXStyle.TextInputLayout.ExposedMenu"
android:id="@+id/input_entry_hardware_key" android:id="@+id/input_entry_hardware_key_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/hardware_key"> android:hint="@string/hardware_key">

View File

@@ -161,6 +161,7 @@
<string name="error_no_name">Enter a name.</string> <string name="error_no_name">Enter a name.</string>
<string name="error_word_reserved">This word is reserved and cannot be used.</string> <string name="error_word_reserved">This word is reserved and cannot be used.</string>
<string name="error_nokeyfile">Select a keyfile.</string> <string name="error_nokeyfile">Select a keyfile.</string>
<string name="error_no_hardware_key">Select a hardware key.</string>
<string name="error_out_of_memory">No memory to load your entire database.</string> <string name="error_out_of_memory">No memory to load your entire database.</string>
<string name="error_load_database">Could not load your database.</string> <string name="error_load_database">Could not load your database.</string>
<string name="error_load_database_KDF_memory">Could not load the key. Try to lower the KDF \"Memory Usage\".</string> <string name="error_load_database_KDF_memory">Could not load the key. Try to lower the KDF \"Memory Usage\".</string>
@@ -355,6 +356,7 @@
<string name="warning_database_revoked">Access to the file revoked by the file manager, close the database and reopen it from its location.</string> <string name="warning_database_revoked">Access to the file revoked by the file manager, close the database and reopen it from its location.</string>
<string name="warning_exact_alarm">You have not allowed the app to use an exact alarm. As a result, the features requiring a timer will not be done with an exact time.</string> <string name="warning_exact_alarm">You have not allowed the app to use an exact alarm. As a result, the features requiring a timer will not be done with an exact time.</string>
<string name="warning_keyfile_integrity">The hash of the file is not guaranteed because Android can change its data on the fly. Change the file extension to .bin for correct integrity.</string> <string name="warning_keyfile_integrity">The hash of the file is not guaranteed because Android can change its data on the fly. Change the file extension to .bin for correct integrity.</string>
<string name="warning_hardware_key_required">Driver for this hardware is required.</string>
<string name="permission">Permission</string> <string name="permission">Permission</string>
<string name="version_label">Version %1$s</string> <string name="version_label">Version %1$s</string>
<string name="build_label">Build %1$s</string> <string name="build_label">Build %1$s</string>