From ecbee73eae3b583be68e9674362c813ee3b08762 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 21 Apr 2022 18:03:32 +0200 Subject: [PATCH] Add view and first implementation of hardware key #8 --- .../activities/MainCredentialActivity.kt | 60 +++++++- .../dialogs/MainCredentialDialogFragment.kt | 2 +- .../SetMainCredentialDialogFragment.kt | 2 +- .../kunzisoft/keepass/hardware/HardwareKey.kt | 33 +++++ .../hardware/HardwareKeyResponseHelper.kt | 125 +++++++++++++++++ .../keepass/view/HardwareKeySelectionView.kt | 130 ++++++++++++++++++ .../keepass/view/MainCredentialView.kt | 65 ++++++--- .../layout/fragment_set_main_credential.xml | 35 ++++- .../layout/view_hardware_key_selection.xml | 26 ++++ .../main/res/layout/view_main_credentials.xml | 34 ++++- app/src/main/res/values/strings.xml | 2 + 11 files changed, 482 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt create mode 100644 app/src/main/res/layout/view_hardware_key_selection.xml diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index bc4e50870..3c536a7c6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -59,6 +59,8 @@ import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.education.PasswordActivityEducation +import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY @@ -101,6 +103,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private var mRememberKeyFile: Boolean = false private var mExternalFileHelper: ExternalFileHelper? = null + private var mHardwareKeyResponseHelper: HardwareKeyResponseHelper? = null + private var mReadOnly: Boolean = false private var mForceReadOnly: Boolean = false @@ -134,10 +138,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu } mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) - mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity) + // Build elements to manage keyfile selection + mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper?.buildOpenDocument { uri -> if (uri != null) { - mainCredentialView?.populateKeyFileTextView(uri) + mainCredentialView?.populateKeyFileView(uri) } } mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper) @@ -145,6 +150,35 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu loadDatabase() } + // Build elements to manage hardware key + mHardwareKeyResponseHelper = HardwareKeyResponseHelper(this) + mHardwareKeyResponseHelper?.buildHardwareKeyResponse { responseData -> + mainCredentialView?.challengeResponse = responseData + } + mainCredentialView?.onRequestHardwareKeyResponse = { hardwareKey -> + try { + when (hardwareKey) { + HardwareKey.HMAC_SHA1_KPXC -> { + mDatabaseFileUri?.let { databaseUri -> + mHardwareKeyResponseHelper?.launchChallengeForResponse(databaseUri) + } + } + else -> { + // TODO other algorithm + } + } + } catch (e: Exception) { + Log.e(TAG, "Unable to retrieve the challenge response", e) + e.message?.let { message -> + Snackbar.make( + coordinatorLayout, + message, + Snackbar.LENGTH_LONG + ).asError().show() + } + } + } + // If is a view intent getUriFromIntent(intent) @@ -171,6 +205,16 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu mAdvancedUnlockViewModel.checkUnlockAvailability() enableConfirmationButton() } + mainCredentialView?.onKeyFileChecked = + CompoundButton.OnCheckedChangeListener { _, _ -> + // TODO mAdvancedUnlockViewModel.checkUnlockAvailability() + enableConfirmationButton() + } + mainCredentialView?.onHardwareKeyChecked = + CompoundButton.OnCheckedChangeListener { _, _ -> + // TODO mAdvancedUnlockViewModel.checkUnlockAvailability() + enableConfirmationButton() + } // Observe if default database mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> @@ -335,11 +379,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu if (action != null && action == VIEW_INTENT) { mDatabaseFileUri = intent.data - mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE)) + mainCredentialView?.populateKeyFileView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE)) } else { mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME) intent?.getParcelableExtra(KEY_KEYFILE)?.let { - mainCredentialView?.populateKeyFileTextView(it) + mainCredentialView?.populateKeyFileView(it) } } try { @@ -436,11 +480,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) { // Define Key File text if (mRememberKeyFile) { - mainCredentialView?.populateKeyFileTextView(keyFileUri) + mainCredentialView?.populateKeyFileView(keyFileUri) } // Define listener for validate button - confirmButtonView?.setOnClickListener { loadDatabase() } + confirmButtonView?.setOnClickListener { + mainCredentialView?.validateCredential() + } // If Activity is launch with a password and want to open directly val intent = intent @@ -475,7 +521,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { mainCredentialView?.populatePasswordTextView(null) if (clearKeyFile) { - mainCredentialView?.populateKeyFileTextView(null) + mainCredentialView?.populateKeyFileView(null) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt index d45c3ea97..2180caa8a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt @@ -95,7 +95,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() { mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper?.buildOpenDocument { uri -> if (uri != null) { - mainCredentialView?.populateKeyFileTextView(uri) + mainCredentialView?.populateKeyFileView(uri) } } mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt index e654c0817..661cdd22e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt @@ -138,7 +138,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { passwordRepeatView = rootView?.findViewById(R.id.password_confirmation) passwordRepeatView?.applyFontVisibility() - keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox) + keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkbox) keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) mExternalFileHelper = ExternalFileHelper(this) diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt new file mode 100644 index 000000000..f483857c1 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt @@ -0,0 +1,33 @@ +package com.kunzisoft.keepass.hardware + +enum class HardwareKey(val value: String) { + HMAC_SHA1_KPXC("HMAC-SHA1 KPXC"), + HMAC_SHA1_KP2("HMAC-SHA1 KP2"), + OATH_HOTP("OATH HOTP"), + HMAC_SECRET_FIDO2("HMAC-SECRET FIDO2"); + + companion object { + val DEFAULT = HMAC_SHA1_KPXC + + fun getStringValues(): List { + return values().map { it.value } + } + + fun fromPosition(position: Int): HardwareKey { + return when (position) { + 0 -> HMAC_SHA1_KPXC + 1 -> HMAC_SHA1_KP2 + 2 -> OATH_HOTP + 3 -> HMAC_SECRET_FIDO2 + else -> DEFAULT + } + } + + fun getHardwareKeyFromString(text: String): HardwareKey { + values().find { it.value == text }?.let { + return it + } + return DEFAULT + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt new file mode 100644 index 000000000..e501670bb --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt @@ -0,0 +1,125 @@ +package com.kunzisoft.keepass.hardware + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri +import android.util.Log +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import com.kunzisoft.keepass.database.element.database.DatabaseKDBX +import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX +import com.kunzisoft.keepass.utils.UriUtil +import java.io.BufferedInputStream +import java.io.IOException +import java.io.InputStream + +class HardwareKeyResponseHelper { + + private var activity: FragmentActivity? = null + private var fragment: Fragment? = null + + private var getChallengeResponseResultLauncher: ActivityResultLauncher? = null + + constructor(context: FragmentActivity) { + this.activity = context + this.fragment = null + } + + constructor(context: Fragment) { + this.activity = context.activity + this.fragment = context + } + + fun buildHardwareKeyResponse(onChallengeResponded: ((challengeResponse:ByteArray?) -> Unit)?) { + val resultCallback = ActivityResultCallback { 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()) + challengeResponse?.let { + onChallengeResponded?.invoke(challengeResponse) + } + } + } + + getChallengeResponseResultLauncher = if (fragment != null) { + fragment?.registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + resultCallback + ) + } else { + activity?.registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + resultCallback + ) + } + } + + fun launchChallengeForResponse(databaseUri: Uri) { + fragment?.context?.contentResolver ?: activity?.contentResolver ?.let { contentResolver -> + getTransformSeedFromHeader(databaseUri, contentResolver)?.let { seed -> + // seed: 32 byte transform seed, needs to be padded before sent to the hardware + val challenge = ByteArray(64) + System.arraycopy(seed, 0, challenge, 0, 32) + challenge.fill(32, 32, 64) + val intent = Intent("net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE") + Log.d(TAG, "Challenge sent to yubikey: " + challenge.contentToString()) + intent.putExtra("challenge", challenge) + try { + getChallengeResponseResultLauncher?.launch(intent) + } catch (e: ActivityNotFoundException) { + // TODO better error + throw IOException("No activity to handle CHALLENGE_RESPONSE intent") + } + } + } + } + + private fun getTransformSeedFromHeader(uri: Uri, contentResolver: ContentResolver): ByteArray? { + // TODO better implementation + var databaseInputStream: InputStream? = null + var challenge: ByteArray? = null + + try { + // Load Data, pass Uris as InputStreams + val databaseStream = UriUtil.getUriInputStream(contentResolver, uri) + ?: throw IOException("Database input stream cannot be retrieve") + + databaseInputStream = BufferedInputStream(databaseStream) + if (!databaseInputStream.markSupported()) { + throw IOException("Input stream does not support mark.") + } + + // We'll end up reading 8 bytes to identify the header. Might as well use two extra. + databaseInputStream.mark(10) + + // Return to the start + databaseInputStream.reset() + + val header = DatabaseHeaderKDBX(DatabaseKDBX()) + + header.loadFromFile(databaseInputStream) + + challenge = ByteArray(64) + System.arraycopy(header.transformSeed, 0, challenge, 0, 32) + challenge.fill(32, 32, 64) + + } catch (e: Exception) { + Log.e(TAG, "Could not read transform seed from file") + } finally { + databaseInputStream?.close() + } + + return challenge + } + + companion object { + private val TAG = HardwareKeyResponseHelper::class.java.simpleName + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt new file mode 100644 index 000000000..88cfddf0b --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt @@ -0,0 +1,130 @@ +package com.kunzisoft.keepass.view + +import android.content.Context +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.Creator +import android.text.InputType +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Filter +import androidx.appcompat.widget.AppCompatAutoCompleteTextView +import androidx.constraintlayout.widget.ConstraintLayout +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.utils.readEnum +import com.kunzisoft.keepass.utils.writeEnum + + +class HardwareKeySelectionView @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0) + : ConstraintLayout(context, attrs, defStyle) { + + private var mHardwareKey: HardwareKey = HardwareKey.DEFAULT + + private val hardwareKeyCompletion: AppCompatAutoCompleteTextView + var selectionListener: ((HardwareKey)-> Unit)? = null + + private val mHardwareKeyAdapter = ArrayAdapterNoFilter(context) + + private class ArrayAdapterNoFilter(context: Context) + : ArrayAdapter(context, android.R.layout.simple_list_item_1) { + val hardwareKeys = HardwareKey.values() + + override fun getCount(): Int { + return hardwareKeys.size + } + + override fun getItem(position: Int): String { + return hardwareKeys[position].value + } + + override fun getItemId(position: Int): Long { + // Or just return p0 + return hardwareKeys[position].hashCode().toLong() + } + + override fun getFilter(): Filter { + return object : Filter() { + override fun performFiltering(p0: CharSequence?): FilterResults { + return FilterResults().apply { + values = hardwareKeys + } + } + + override fun publishResults(p0: CharSequence?, p1: FilterResults?) { + notifyDataSetChanged() + } + } + } + } + + init { + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? + inflater?.inflate(R.layout.view_hardware_key_selection, this) + + hardwareKeyCompletion = findViewById(R.id.input_entry_hardware_key_completion) + + hardwareKeyCompletion.inputType = InputType.TYPE_NULL + hardwareKeyCompletion.setAdapter(mHardwareKeyAdapter) + + hardwareKeyCompletion.onItemClickListener = + AdapterView.OnItemClickListener { _, _, position, _ -> + mHardwareKey = HardwareKey.fromPosition(position) + selectionListener?.invoke(mHardwareKey) + } + } + + var hardwareKey: HardwareKey + get() { + return mHardwareKey + } + set(value) { + mHardwareKey = value + hardwareKeyCompletion.setSelection(value.ordinal) + } + + override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + val saveState = SavedState(superState) + saveState.mHardwareKey = this.mHardwareKey + return saveState + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state !is SavedState) { + super.onRestoreInstanceState(state) + return + } + super.onRestoreInstanceState(state.superState) + this.mHardwareKey = state.mHardwareKey + } + + internal class SavedState : BaseSavedState { + var mHardwareKey: HardwareKey = HardwareKey.DEFAULT + + constructor(superState: Parcelable?) : super(superState) + + private constructor(parcel: Parcel) : super(parcel) { + mHardwareKey = parcel.readEnum() ?: HardwareKey.DEFAULT + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeEnum(mHardwareKey) + } + + companion object CREATOR : Creator { + override fun createFromParcel(parcel: Parcel): SavedState { + return SavedState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt index 75ba32eb4..11022f526 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt @@ -30,14 +30,12 @@ import android.view.KeyEvent import android.view.LayoutInflater import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager -import android.widget.CompoundButton -import android.widget.EditText -import android.widget.FrameLayout -import android.widget.TextView +import android.widget.* import androidx.appcompat.app.AppCompatActivity 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.model.CredentialStorage import com.kunzisoft.keepass.model.MainCredential @@ -46,13 +44,18 @@ class MainCredentialView @JvmOverloads constructor(context: Context, defStyle: Int = 0) : FrameLayout(context, attrs, defStyle) { - private var passwordTextView: EditText - private var keyFileSelectionView: KeyFileSelectionView private var checkboxPasswordView: CompoundButton + private var passwordTextView: EditText private var checkboxKeyFileView: CompoundButton + private var keyFileSelectionView: KeyFileSelectionView + private var checkboxHardwareView: CompoundButton + private var hardwareKeySelectionView: HardwareKeySelectionView var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null + var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null + var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null var onValidateListener: (() -> Unit)? = null + var onRequestHardwareKeyResponse: ((HardwareKey)-> Unit)? = null private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD @@ -60,15 +63,17 @@ class MainCredentialView @JvmOverloads constructor(context: Context, val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? inflater?.inflate(R.layout.view_main_credentials, this) - passwordTextView = findViewById(R.id.password_text_view) - keyFileSelectionView = findViewById(R.id.keyfile_selection) checkboxPasswordView = findViewById(R.id.password_checkbox) - checkboxKeyFileView = findViewById(R.id.keyfile_checkox) + passwordTextView = findViewById(R.id.password_text_view) + checkboxKeyFileView = findViewById(R.id.keyfile_checkbox) + keyFileSelectionView = findViewById(R.id.keyfile_selection) + checkboxHardwareView = findViewById(R.id.hardware_key_checkbox) + hardwareKeySelectionView = findViewById(R.id.hardware_key_selection) val onEditorActionListener = object : TextView.OnEditorActionListener { override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { if (actionId == EditorInfo.IME_ACTION_DONE) { - onValidateListener?.invoke() + validateCredential() return true } return false @@ -91,7 +96,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context, if (keyEvent.action == KeyEvent.ACTION_DOWN && keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER ) { - onValidateListener?.invoke() + validateCredential() handled = true } handled @@ -100,10 +105,25 @@ class MainCredentialView @JvmOverloads constructor(context: Context, checkboxPasswordView.setOnCheckedChangeListener { view, checked -> onPasswordChecked?.onCheckedChanged(view, checked) } + checkboxKeyFileView.setOnCheckedChangeListener { view, checked -> + onKeyFileChecked?.onCheckedChanged(view, checked) + } + checkboxHardwareView.setOnCheckedChangeListener { view, checked -> + onHardwareKeyChecked?.onCheckedChanged(view, checked) + } + + hardwareKeySelectionView.selectionListener = { _ -> + checkboxHardwareView.isChecked = true + } } - fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) { - keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper) + fun validateCredential() { + val hardwareKey = hardwareKeySelectionView.hardwareKey + if (checkboxHardwareView.isChecked) { + onRequestHardwareKeyResponse?.invoke(hardwareKey) + } else { + onValidateListener?.invoke() + } } fun populatePasswordTextView(text: String?) { @@ -118,7 +138,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context, } } - fun populateKeyFileTextView(uri: Uri?) { + fun populateKeyFileView(uri: Uri?) { if (uri == null || uri.toString().isEmpty()) { keyFileSelectionView.uri = null if (checkboxKeyFileView.isChecked) @@ -130,16 +150,27 @@ class MainCredentialView @JvmOverloads constructor(context: Context, } } - fun isFill(): Boolean { - return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked + fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) { + keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper) } + fun isFill(): Boolean { + return checkboxPasswordView.isChecked + || checkboxKeyFileView.isChecked // TODO better recognition + || checkboxHardwareView.isChecked + } + + // TODO Challenge response + var challengeResponse: ByteArray? = null + fun getMainCredential(): MainCredential { return MainCredential().apply { this.masterPassword = if (checkboxPasswordView.isChecked) passwordTextView.text?.toString() else null this.keyFileUri = if (checkboxKeyFileView.isChecked) keyFileSelectionView.uri else null + this.hardwareKeyData = if (checkboxHardwareView.isChecked) + challengeResponse else null } } @@ -151,7 +182,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context, // TODO HARDWARE_KEY return when (mCredentialStorage) { CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked - CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked + CredentialStorage.KEY_FILE -> false CredentialStorage.HARDWARE_KEY -> false } } diff --git a/app/src/main/res/layout/fragment_set_main_credential.xml b/app/src/main/res/layout/fragment_set_main_credential.xml index 343124bdb..b17de8a52 100644 --- a/app/src/main/res/layout/fragment_set_main_credential.xml +++ b/app/src/main/res/layout/fragment_set_main_credential.xml @@ -115,7 +115,7 @@ android:orientation="vertical"> @@ -126,7 +126,38 @@ android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toEndOf="@+id/keyfile_checkox" + app:layout_constraintStart_toEndOf="@+id/keyfile_checkbox" + app:layout_constraintEnd_toEndOf="parent" /> + + + + + + + + + diff --git a/app/src/main/res/layout/view_hardware_key_selection.xml b/app/src/main/res/layout/view_hardware_key_selection.xml new file mode 100644 index 000000000..2c441ef5e --- /dev/null +++ b/app/src/main/res/layout/view_hardware_key_selection.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_main_credentials.xml b/app/src/main/res/layout/view_main_credentials.xml index e0f9ff64e..41d632516 100644 --- a/app/src/main/res/layout/view_main_credentials.xml +++ b/app/src/main/res/layout/view_main_credentials.xml @@ -62,7 +62,7 @@ android:layout_height="wrap_content"> + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 556135f33..adf076d03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,6 +56,7 @@ One-time password info Password checkbox Keyfile checkbox + Hardware key checkbox Repeat toggle password visibility Entry icon Database color @@ -96,6 +97,7 @@ History Attachments Keyfile + Hardware key Modified Searchable Inherit