Add view and first implementation of hardware key #8

This commit is contained in:
J-Jamet
2022-04-21 18:03:32 +02:00
parent 1874a0056d
commit ecbee73eae
11 changed files with 482 additions and 32 deletions

View File

@@ -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<Uri?>(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)
}
}

View File

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

View File

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

View File

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

View File

@@ -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<Intent>? = 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<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())
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
}
}

View File

@@ -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<String>(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>() ?: HardwareKey.DEFAULT
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeEnum(mHardwareKey)
}
companion object CREATOR : Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}

View File

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

View File

@@ -115,7 +115,7 @@
android:orientation="vertical">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox"
android:id="@+id/keyfile_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_keyfile"/>
@@ -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" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_view_hardware_key"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_margin"
android:orientation="vertical">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/hardware_key_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hardware_key"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/hardware_key_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/hardware_key_checkbox"
app:layout_constraintEnd_toEndOf="parent" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container_hardware_key"
android:layout_marginBottom="@dimen/default_margin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants"
android:importantForAccessibility="no"
tools:ignore="UnusedAttribute">
<com.google.android.material.textfield.TextInputLayout
style="@style/KeepassDXStyle.TextInputLayout.ExposedMenu"
android:id="@+id/input_entry_hardware_key"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hardware_key">
<androidx.appcompat.widget.AppCompatAutoCompleteTextView
android:id="@+id/input_entry_hardware_key_completion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>

View File

@@ -62,7 +62,7 @@
android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox"
android:id="@+id/keyfile_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/keyfile_selection"
@@ -75,9 +75,35 @@
android:id="@+id/keyfile_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_toRightOf="@+id/keyfile_checkox"
android:layout_toEndOf="@+id/keyfile_checkox"
android:layout_toEndOf="@+id/keyfile_checkbox"
android:layout_toRightOf="@+id/keyfile_checkbox"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:minHeight="48dp" />
</RelativeLayout>
<!-- Hardware key -->
<RelativeLayout
android:id="@+id/container_hardware_key"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/hardware_key_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/hardware_key_selection"
android:layout_marginTop="22dp"
android:contentDescription="@string/content_description_hardware_key_checkbox"
android:focusable="false"
android:gravity="center_vertical" />
<com.kunzisoft.keepass.view.HardwareKeySelectionView
android:id="@+id/hardware_key_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/hardware_key_checkbox"
android:layout_toRightOf="@+id/hardware_key_checkbox"
android:importantForAccessibility="no"
android:importantForAutofill="no" />
</RelativeLayout>

View File

@@ -56,6 +56,7 @@
<string name="content_description_otp_information">One-time password info</string>
<string name="content_description_password_checkbox">Password checkbox</string>
<string name="content_description_keyfile_checkbox">Keyfile checkbox</string>
<string name="content_description_hardware_key_checkbox">Hardware key checkbox</string>
<string name="content_description_repeat_toggle_password_visibility">Repeat toggle password visibility</string>
<string name="content_description_entry_icon">Entry icon</string>
<string name="content_description_database_color">Database color</string>
@@ -96,6 +97,7 @@
<string name="entry_history">History</string>
<string name="entry_attachments">Attachments</string>
<string name="entry_keyfile">Keyfile</string>
<string name="hardware_key">Hardware key</string>
<string name="entry_modified">Modified</string>
<string name="searchable">Searchable</string>
<string name="inherited">Inherit</string>