From f8d80525d9bb95904a93d0bf2776b6e7783230b3 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Mon, 13 Oct 2025 10:32:43 +0200 Subject: [PATCH] fix: Hardware key #2196 --- app/src/main/AndroidManifest.xml | 13 +- .../SetMainCredentialDialogFragment.kt | 2 +- .../activity}/HardwareKeyActivity.kt | 129 ++++++++-------- .../viewmodel/CredentialLauncherViewModel.kt | 4 +- .../viewmodel/HardwareKeyLauncherViewModel.kt | 144 ++++++++++++++++++ .../DatabaseTaskNotificationService.kt | 2 +- 6 files changed, 216 insertions(+), 78 deletions(-) rename app/src/main/java/com/kunzisoft/keepass/{hardware => credentialprovider/activity}/HardwareKeyActivity.kt (51%) create mode 100644 app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/HardwareKeyLauncherViewModel.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c4dd5bef2..66cb1d194 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -178,19 +178,22 @@ + android:exported="false" + android:excludeFromRecents="true" /> + android:exported="false" + android:excludeFromRecents="true" /> + android:exported="true" + android:excludeFromRecents="true"> @@ -209,8 +212,8 @@ android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity" android:theme="@style/Theme.Transparent" android:configChanges="keyboardHidden" - android:excludeFromRecents="true" android:exported="false" + android:excludeFromRecents="true" tools:targetApi="upside_down_cake" /> { 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 val mHardwareKeyLauncherViewModel: HardwareKeyLauncherViewModel by viewModels() - private var activityResultLauncher: ActivityResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - resultCallback - ) + private var activityResultLauncher: ActivityResultLauncher = + this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + mHardwareKeyLauncherViewModel.manageSelectionResult(it) + } override fun applyCustomStyle(): Boolean { return false @@ -48,65 +45,60 @@ class HardwareKeyActivity: DatabaseModeActivity(){ return false } - override fun onDatabaseRetrieved(database: ContextualDatabase) { - val hardwareKey = HardwareKey.getHardwareKeyFromString( - intent.getStringExtra(DATA_HARDWARE_KEY) - ) - if (isHardwareKeyAvailable(this, hardwareKey, true) { - mDatabaseViewModel.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 -> { - finish() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + lifecycleScope.launch { + mHardwareKeyLauncherViewModel.uiState.collect { uiState -> + when (uiState) { + is HardwareKeyLauncherViewModel.UIState.Loading -> {} + is HardwareKeyLauncherViewModel.UIState.LaunchChallengeActivityForResponse -> { + // Send to the driver + activityResultLauncher.launch( + buildHardwareKeyChallenge(uiState.challenge) + ) + } + is HardwareKeyLauncherViewModel.UIState.OnChallengeResponded -> { + mDatabaseViewModel.onChallengeResponded(uiState.response) + } } } } } - 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) - } - // Send to the driver - activityResultLauncher.launch( - Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply { - putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge) + override fun onDatabaseRetrieved(database: ContextualDatabase) { + super.onDatabaseRetrieved(database) + mHardwareKeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database) + } + + override fun onDatabaseActionFinished( + database: ContextualDatabase, + actionTask: String, + result: ActionRunnable.Result + ) { + super.onDatabaseActionFinished(database, actionTask, result) + when (actionTask) { + ACTION_DATABASE_LOAD_TASK -> { + finish() } - ) - Log.d(TAG, "Challenge sent") + } } companion object { 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( context: Context, hardwareKey: HardwareKey, seed: ByteArray? ) { - context.startActivity(Intent(context, HardwareKeyActivity::class.java).apply { - flags = FLAG_ACTIVITY_NEW_TASK - putExtra(DATA_HARDWARE_KEY, hardwareKey.value) - putExtra(DATA_SEED, seed) + context.startActivity( + Intent( + context, + 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 -> { // Check available intent - val yubikeyDriverAvailable = - Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT) - .resolveActivity(context.packageManager) != null - if (showDialog && !yubikeyDriverAvailable - && context is Activity) + // TODO Dialog + val yubikeyDriverAvailable = isYubikeyDriverAvailable(context) + if (showDialog && !yubikeyDriverAvailable && context is Activity) { showHardwareKeyDriverNeeded(context, hardwareKey) { onDialogDismissed?.onDismiss(it) context.finish() } + } yubikeyDriverAvailable } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt index f6d3033a0..7eb876f2a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/CredentialLauncherViewModel.kt @@ -82,7 +82,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie mSelectionResult = null } - abstract fun manageRegistrationResult(activityResult: ActivityResult) + open fun manageRegistrationResult(activityResult: ActivityResult) {} open fun onExceptionOccurred(e: Throwable) { showError(e) @@ -93,7 +93,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie specialMode: SpecialMode, database: ContextualDatabase? ) { - if (database != null && database.loaded) { + if (database != null) { onDatabaseRetrieved(database) } if (isResultLauncherRegistered.not()) { diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/HardwareKeyLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/HardwareKeyLauncherViewModel.kt new file mode 100644 index 000000000..a7fdbcfd0 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/HardwareKeyLauncherViewModel.kt @@ -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.Loading) + val uiState: StateFlow = 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt index 4bfcb93e1..40f07cd28 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -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.Type 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.SnapFileDatabaseInfo import com.kunzisoft.keepass.settings.PreferencesUtil