mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Add view and first implementation of hardware key #8
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
|
||||
hardwareKeySelectionView.selectionListener = { _ ->
|
||||
checkboxHardwareView.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
26
app/src/main/res/layout/view_hardware_key_selection.xml
Normal file
26
app/src/main/res/layout/view_hardware_key_selection.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user