fix: Hardware key #2196

This commit is contained in:
J-Jamet
2025-10-13 10:32:43 +02:00
parent 65857596a6
commit f8d80525d9
6 changed files with 216 additions and 78 deletions

View File

@@ -178,19 +178,22 @@
<activity <activity
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
<activity <activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:exported="false" /> android:exported="false"
android:excludeFromRecents="true" />
<activity <activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:excludeFromRecents="true"/> android:exported="false"
android:excludeFromRecents="true" />
<activity <activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:exported="true"> android:exported="true"
android:excludeFromRecents="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@@ -209,8 +212,8 @@
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity" android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:excludeFromRecents="true"
android:exported="false" android:exported="false"
android:excludeFromRecents="true"
tools:targetApi="upside_down_cake" /> tools:targetApi="upside_down_cake" />
<service <service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService" android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"

View File

@@ -37,7 +37,7 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl import com.kunzisoft.keepass.utils.UriUtil.openUrl

View File

@@ -1,20 +1,28 @@
package com.kunzisoft.keepass.hardware package com.kunzisoft.keepass.credentialprovider.activity
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Bundle
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addHardwareKey
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addSeed
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.buildHardwareKeyChallenge
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.isYubikeyDriverAvailable
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
import kotlinx.coroutines.launch
/** /**
* Special activity to deal with hardware key drivers, * Special activity to deal with hardware key drivers,
@@ -22,23 +30,12 @@ import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
*/ */
class HardwareKeyActivity: DatabaseModeActivity(){ class HardwareKeyActivity: DatabaseModeActivity(){
// To manage hardware key challenge response private val mHardwareKeyLauncherViewModel: HardwareKeyLauncherViewModel by viewModels()
private val resultCallback = ActivityResultCallback<ActivityResult> { result ->
if (result.resultCode == RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
mDatabaseViewModel.onChallengeResponded(challengeResponse)
} else {
Log.e(TAG, "Response from challenge error")
mDatabaseViewModel.onChallengeResponded(null)
}
finish()
}
private var activityResultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult( private var activityResultLauncher: ActivityResultLauncher<Intent> =
ActivityResultContracts.StartActivityForResult(), this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
resultCallback mHardwareKeyLauncherViewModel.manageSelectionResult(it)
) }
override fun applyCustomStyle(): Boolean { override fun applyCustomStyle(): Boolean {
return false return false
@@ -48,65 +45,60 @@ class HardwareKeyActivity: DatabaseModeActivity(){
return false return false
} }
override fun onDatabaseRetrieved(database: ContextualDatabase) { override fun onCreate(savedInstanceState: Bundle?) {
val hardwareKey = HardwareKey.getHardwareKeyFromString( super.onCreate(savedInstanceState)
intent.getStringExtra(DATA_HARDWARE_KEY)
) lifecycleScope.launch {
if (isHardwareKeyAvailable(this, hardwareKey, true) { mHardwareKeyLauncherViewModel.uiState.collect { uiState ->
mDatabaseViewModel.onChallengeResponded(null) when (uiState) {
}) { is HardwareKeyLauncherViewModel.UIState.Loading -> {}
when (hardwareKey) { is HardwareKeyLauncherViewModel.UIState.LaunchChallengeActivityForResponse -> {
/* // Send to the driver
HardwareKey.FIDO2_SECRET -> { activityResultLauncher.launch(
// TODO FIDO2 under development buildHardwareKeyChallenge(uiState.challenge)
throw Exception("FIDO2 not implemented") )
} }
*/ is HardwareKeyLauncherViewModel.UIState.OnChallengeResponded -> {
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { mDatabaseViewModel.onChallengeResponded(uiState.response)
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED)) }
}
else -> {
finish()
} }
} }
} }
} }
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) { override fun onDatabaseRetrieved(database: ContextualDatabase) {
// Transform the seed before sending super.onDatabaseRetrieved(database)
var challenge: ByteArray? = null mHardwareKeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
if (seed != null) { }
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32) override fun onDatabaseActionFinished(
challenge.fill(32, 32, 64) database: ContextualDatabase,
} actionTask: String,
// Send to the driver result: ActionRunnable.Result
activityResultLauncher.launch( ) {
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply { super.onDatabaseActionFinished(database, actionTask, result)
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge) when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
finish()
} }
) }
Log.d(TAG, "Challenge sent")
} }
companion object { companion object {
private val TAG = HardwareKeyActivity::class.java.simpleName private val TAG = HardwareKeyActivity::class.java.simpleName
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
private const val DATA_SEED = "DATA_SEED"
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
fun launchHardwareKeyActivity( fun launchHardwareKeyActivity(
context: Context, context: Context,
hardwareKey: HardwareKey, hardwareKey: HardwareKey,
seed: ByteArray? seed: ByteArray?
) { ) {
context.startActivity(Intent(context, HardwareKeyActivity::class.java).apply { context.startActivity(
flags = FLAG_ACTIVITY_NEW_TASK Intent(
putExtra(DATA_HARDWARE_KEY, hardwareKey.value) context,
putExtra(DATA_SEED, seed) HardwareKeyActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
addHardwareKey(hardwareKey)
addSeed(seed)
}) })
} }
@@ -130,15 +122,14 @@ class HardwareKeyActivity: DatabaseModeActivity(){
*/ */
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent // Check available intent
val yubikeyDriverAvailable = // TODO Dialog
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT) val yubikeyDriverAvailable = isYubikeyDriverAvailable(context)
.resolveActivity(context.packageManager) != null if (showDialog && !yubikeyDriverAvailable && context is Activity) {
if (showDialog && !yubikeyDriverAvailable
&& context is Activity)
showHardwareKeyDriverNeeded(context, hardwareKey) { showHardwareKeyDriverNeeded(context, hardwareKey) {
onDialogDismissed?.onDismiss(it) onDialogDismissed?.onDismiss(it)
context.finish() context.finish()
} }
}
yubikeyDriverAvailable yubikeyDriverAvailable
} }
} }

View File

@@ -82,7 +82,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
mSelectionResult = null mSelectionResult = null
} }
abstract fun manageRegistrationResult(activityResult: ActivityResult) open fun manageRegistrationResult(activityResult: ActivityResult) {}
open fun onExceptionOccurred(e: Throwable) { open fun onExceptionOccurred(e: Throwable) {
showError(e) showError(e)
@@ -93,7 +93,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
specialMode: SpecialMode, specialMode: SpecialMode,
database: ContextualDatabase? database: ContextualDatabase?
) { ) {
if (database != null && database.loaded) { if (database != null) {
onDatabaseRetrieved(database) onDatabaseRetrieved(database)
} }
if (isResultLauncherRegistered.not()) { if (isResultLauncherRegistered.not()) {

View File

@@ -0,0 +1,144 @@
package com.kunzisoft.keepass.credentialprovider.viewmodel
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity.Companion.isHardwareKeyAvailable
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.hardware.HardwareKey
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class HardwareKeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
override suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
val hardwareKey = HardwareKey.Companion.getHardwareKeyFromString(
intent.getStringExtra(DATA_HARDWARE_KEY)
)
if (isHardwareKeyAvailable(getApplication(), hardwareKey, true) {
mUiState.value = UIState.OnChallengeResponded(null)
}) {
when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
}
else -> {
UIState.OnChallengeResponded(null)
}
}
}
}
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
// 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)
}
mUiState.value = UIState.LaunchChallengeActivityForResponse(challenge)
Log.d(TAG, "Challenge sent")
}
override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult
) {
super.manageSelectionResult(database, activityResult)
if (activityResult.resultCode == RESULT_OK) {
val challengeResponse: ByteArray? =
activityResult.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
mUiState.value = UIState.OnChallengeResponded(challengeResponse)
} else {
Log.e(TAG, "Response from challenge error")
mUiState.value = UIState.OnChallengeResponded(null)
}
}
sealed class UIState {
object Loading : UIState()
data class LaunchChallengeActivityForResponse(
val challenge: ByteArray?,
): UIState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LaunchChallengeActivityForResponse
return challenge.contentEquals(other.challenge)
}
override fun hashCode(): Int {
return challenge?.contentHashCode() ?: 0
}
}
data class OnChallengeResponded(
val response: ByteArray?
): UIState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as OnChallengeResponded
return response.contentEquals(other.response)
}
override fun hashCode(): Int {
return response?.contentHashCode() ?: 0
}
}
}
companion object {
private val TAG = HardwareKeyLauncherViewModel::class.java.name
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
private const val DATA_SEED = "DATA_SEED"
// Driver call
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
fun isYubikeyDriverAvailable(context: Context): Boolean {
return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(context.packageManager) != null
}
fun buildHardwareKeyChallenge(challenge: ByteArray?): Intent {
return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
}
fun Intent.addHardwareKey(hardwareKey: HardwareKey) {
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
}
fun Intent.addSeed(seed: ByteArray?) {
putExtra(DATA_SEED, seed)
}
}
}

View File

@@ -61,7 +61,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil