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.setOpenDocumentClickListener
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil
@@ -53,7 +54,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private lateinit var rootView: View
private lateinit var passwordCheckBox: CompoundButton
private lateinit var passKeyView: PassKeyView
private lateinit var passwordView: PassKeyView
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
private lateinit var passwordRepeatView: TextView
@@ -139,7 +140,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
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)
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
passwordRepeatView.applyFontVisibility()
@@ -165,8 +166,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
hardwareKeySelectionView.selectionListener = { _ ->
hardwareKeySelectionView.selectionListener = { hardwareKey ->
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()
@@ -194,18 +202,41 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
private fun approveMainCredential() {
var error = verifyPassword() || verifyKeyFile() || verifyHardwareKey()
if (!passwordCheckBox.isChecked
&& !keyFileCheckBox.isChecked
&& !hardwareKeyCheckBox.isChecked
val errorPassword = verifyPassword()
val errorKeyFile = verifyKeyFile()
val errorHardwareKey = verifyHardwareKey()
// Check all to fill error
var error = errorPassword || errorKeyFile || errorHardwareKey
val hardwareKey = hardwareKeySelectionView.hardwareKey
if (!error
&& (!passwordCheckBox.isChecked)
&& (!keyFileCheckBox.isChecked)
&& (!hardwareKeyCheckBox.isChecked)
) {
error = true
if (mAllowNoMasterKey)
if (mAllowNoMasterKey) {
// show no key dialog if required
showNoKeyConfirmationDialog()
else {
} else {
passwordRepeatTextInputLayout.error =
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) {
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 {
var error = false
passwordRepeatTextInputLayout.error = null
if (passwordCheckBox.isChecked) {
mMasterPassword = passKeyView.passwordString
mMasterPassword = passwordView.passwordString
val confPassword = passwordRepeatView.text.toString()
// Verify that passwords match
@@ -245,21 +257,13 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
// Passwords do not 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
}
private fun verifyKeyFile(): Boolean {
var error = false
keyFileSelectionView.error = null
if (keyFileCheckBox.isChecked) {
keyFileSelectionView.uri?.let { uri ->
mKeyFileUri = uri
@@ -273,25 +277,45 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private fun verifyHardwareKey(): Boolean {
var error = false
hardwareKeySelectionView.error = null
if (hardwareKeyCheckBox.isChecked) {
hardwareKeySelectionView.hardwareKey.let { hardwareKey ->
hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
mHardwareKey = hardwareKey
} ?: run {
error = true
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
}
// TODO verify drivers
// error = true
}
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() {
activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@SetMainCredentialDialogFragment.dismiss()
}
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@SetMainCredentialDialogFragment.dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
mEmptyPasswordConfirmationDialog = builder.create()

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.NodeId
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.HardwareKeyResponseHelper
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.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import kotlinx.coroutines.*
import kotlinx.coroutines.launch
import java.util.*
/**
@@ -134,8 +135,12 @@ class DatabaseTaskProvider {
respondToChallengeIfAllowed()
}
this.requestChallengeListener = object: DatabaseTaskNotificationService.RequestChallengeListener {
override fun onChallengeResponseRequested(hardwareKey: HardwareKey?, seed: ByteArray?) {
mHardwareKeyResponseHelper?.launchChallengeForResponse(hardwareKey, seed)
override fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?) {
if (HardwareKeyResponseHelper.isHardwareKeyAvailable(activity, hardwareKey)) {
mHardwareKeyResponseHelper?.launchChallengeForResponse(hardwareKey, seed)
} else {
throw InvalidCredentialsDatabaseException("Driver for $hardwareKey is required.")
}
}
}
} else {

View File

@@ -4,13 +4,17 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
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 {
@@ -32,14 +36,13 @@ class HardwareKeyResponseHelper {
fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?,
extra: Bundle?) -> Unit) {
val resultCallback = ActivityResultCallback<ActivityResult> { result ->
Log.d(TAG, "resultCode from ykdroid: " + result.resultCode)
if (result.resultCode == Activity.RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra("response")
Log.d(TAG, "Response: " + challengeResponse.contentToString())
Log.d(TAG, "Response form challenge : " + challengeResponse.contentToString())
onChallengeResponded.invoke(challengeResponse,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
} else {
Log.e(TAG, "Response error")
Log.e(TAG, "Response from challenge error")
onChallengeResponded.invoke(null,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
}
@@ -58,39 +61,25 @@ class HardwareKeyResponseHelper {
}
}
fun launchChallengeForResponse(hardwareKey: HardwareKey?, seed: ByteArray?) {
try {
when (hardwareKey) {
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
// Send to the driver
getChallengeResponseResultLauncher?.launch(Intent(YKDROID_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(YKDROID_SEED_KEY, challenge)
})
Log.d(TAG, "Challenge sent : " + challenge.contentToString())
}
else -> {
// TODO other algorithm
}
fun launchChallengeForResponse(hardwareKey: HardwareKey, seed: ByteArray?) {
when (hardwareKey) {
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2
throw Exception("FIDO2 not implemented")
}
} 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()
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
// Send to the driver
getChallengeResponseResultLauncher!!.launch(Intent(YKDROID_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(YKDROID_SEED_KEY, challenge)
})
Log.d(TAG, "Challenge sent : " + challenge.contentToString())
}
}
}
@@ -98,9 +87,39 @@ class HardwareKeyResponseHelper {
companion object {
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 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 mActionTaskListeners = mutableListOf<ActionTaskListener>()
// 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 mActionRunning = false
@@ -156,7 +156,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
interface RequestChallengeListener {
fun onChallengeResponseRequested(hardwareKey: HardwareKey?, seed: ByteArray?)
fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?)
}
fun checkDatabase() {
@@ -598,7 +598,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
runBlocking {
// Send the request
val challengeResponseRequestListener = mRequestChallengeListenerChannel.receive()
challengeResponseRequestListener?.onChallengeResponseRequested(hardwareKey, seed)
challengeResponseRequestListener.onChallengeResponseRequested(hardwareKey, seed)
// Wait the response
response = mResponseChallengeChannel.receive() ?: byteArrayOf()
}

View File

@@ -269,11 +269,11 @@ object UriUtil {
fun contributingUser(context: Context): Boolean {
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 {
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
Education.setEducationScreenReclickedPerformed(context)
@@ -317,4 +317,6 @@ object 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 androidx.appcompat.widget.AppCompatAutoCompleteTextView
import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.readEnum
@@ -25,8 +26,9 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context,
private var mHardwareKey: HardwareKey? = null
private val hardwareKeyLayout: TextInputLayout
private val hardwareKeyCompletion: AppCompatAutoCompleteTextView
var selectionListener: ((HardwareKey?)-> Unit)? = null
var selectionListener: ((HardwareKey)-> Unit)? = null
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?
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.inputType = InputType.TYPE_NULL
@@ -74,7 +77,9 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context,
hardwareKeyCompletion.onItemClickListener =
AdapterView.OnItemClickListener { _, _, 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)
}
var error: CharSequence?
get() = hardwareKeyLayout.error
set(value) {
hardwareKeyLayout.error = value
}
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val saveState = SavedState(superState)

View File

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

View File

@@ -12,7 +12,7 @@
<com.google.android.material.textfield.TextInputLayout
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_height="wrap_content"
android:hint="@string/hardware_key">

View File

@@ -161,6 +161,7 @@
<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_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_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>
@@ -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_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_hardware_key_required">Driver for this hardware is required.</string>
<string name="permission">Permission</string>
<string name="version_label">Version %1$s</string>
<string name="build_label">Build %1$s</string>